Export event data for invites (#5049)

<!-- PR description-->

---

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

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

#### Type of change

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

#### Issue(s)

<!-- Can reference multiple issues. Use one of the following "magic words" - "closes, fixes" to auto-close the Github issue. -->
* https://github.com/alcionai/corso/issues/5039

#### Test Plan

<!-- How will this be tested prior to merging.-->
- [ ] 💪 Manual
- [x]  Unit test
- [ ] 💚 E2E
This commit is contained in:
Abin Simon 2024-01-22 11:28:49 +05:30 committed by GitHub
parent 6d2d9c0099
commit f1f805b3f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 581 additions and 16 deletions

View File

@ -22,6 +22,7 @@ import (
"github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/internal/common/str"
"github.com/alcionai/corso/src/internal/converters/ics"
"github.com/alcionai/corso/src/pkg/logger"
"github.com/alcionai/corso/src/pkg/services/m365/api"
)
@ -50,6 +51,97 @@ func formatAddress(entry models.EmailAddressable) string {
return fmt.Sprintf(addressFormat, name, email)
}
// getICalData converts the emails to an event so that ical generation
// can generate from it.
func getICalData(ctx context.Context, data models.Messageable) (string, error) {
msg, ok := data.(*models.EventMessageRequest)
if !ok {
return "", clues.NewWC(ctx, "unexpected message type").
With("interface_type", fmt.Sprintf("%T", data))
}
// This method returns nil if data is not pulled using the necessary expand property
// .../messages/<message_id>/?expand=Microsoft.Graph.EventMessage/Event
// Also works for emails which are a result of someone accepting an
// invite. If we add this expand query parameter value when directly
// fetching a cancellation mail, the request fails. It however looks
// to be OK to run when listing emails although it gives empty({})
// event value for cancellations.
// TODO(meain): cancelled event details are available when pulling .eml
if mevent := msg.GetEvent(); mevent != nil {
return ics.FromEventable(ctx, mevent)
}
// Exceptions(modifications) are covered under this, although graph just sends the
// exception event and not the parent, which what eml obtained from graph also contains
if ptr.Val(msg.GetMeetingMessageType()) != models.MEETINGREQUEST_MEETINGMESSAGETYPE {
// We don't have event data if it not "REQUEST" type.
// Both cancellation and acceptance does not return enough
// information to recreate an event.
return "", nil
}
// If data was not fetch with an expand property, then we can
// approximate the details with the following
event := models.NewEvent()
event.SetId(msg.GetId())
event.SetCreatedDateTime(msg.GetCreatedDateTime())
event.SetLastModifiedDateTime(msg.GetLastModifiedDateTime())
event.SetIsAllDay(msg.GetIsAllDay())
event.SetStart(msg.GetStartDateTime())
event.SetEnd(msg.GetEndDateTime())
event.SetRecurrence(msg.GetRecurrence())
// event.SetIsCancelled()
event.SetSubject(msg.GetSubject())
event.SetBodyPreview(msg.GetBodyPreview())
event.SetBody(msg.GetBody())
// https://learn.microsoft.com/en-us/graph/api/resources/eventmessage?view=graph-rest-1.0
// In addition, Outlook automatically creates an event instance in
// the invitee's calendar, with the showAs property as tentative.
event.SetShowAs(ptr.To(models.TENTATIVE_FREEBUSYSTATUS))
event.SetCategories(msg.GetCategories())
event.SetWebLink(msg.GetWebLink())
event.SetOrganizer(msg.GetFrom())
// NOTE: If an event was previously created and we added people to
// it, the original list of attendee are not available.
atts := []models.Attendeeable{}
for _, to := range msg.GetToRecipients() {
att := models.NewAttendee()
att.SetEmailAddress(to.GetEmailAddress())
att.SetTypeEscaped(ptr.To(models.REQUIRED_ATTENDEETYPE))
atts = append(atts, att)
}
for _, cc := range msg.GetCcRecipients() {
att := models.NewAttendee()
att.SetEmailAddress(cc.GetEmailAddress())
att.SetTypeEscaped(ptr.To(models.OPTIONAL_ATTENDEETYPE))
atts = append(atts, att)
}
// bcc did not show up in my tests, but adding for completeness
for _, bcc := range msg.GetBccRecipients() {
att := models.NewAttendee()
att.SetEmailAddress(bcc.GetEmailAddress())
att.SetTypeEscaped(ptr.To(models.OPTIONAL_ATTENDEETYPE))
atts = append(atts, att)
}
event.SetAttendees(atts)
event.SetLocation(msg.GetLocation())
// event.SetSensitivity() // unavailable in msg
event.SetImportance(msg.GetImportance())
// event.SetOnlineMeeting() // not available in eml either
event.SetAttachments(msg.GetAttachments())
return ics.FromEventable(ctx, event)
}
// FromJSON converts a Messageable (as json) to .eml format
func FromJSON(ctx context.Context, body []byte) (string, error) {
ctx = clues.Add(ctx, "body_len", len(body))
@ -62,10 +154,11 @@ func FromJSON(ctx context.Context, body []byte) (string, error) {
ctx = clues.Add(ctx, "item_id", ptr.Val(data.GetId()))
email := mail.NewMSG()
email.AllowDuplicateAddress = true // More "correct" conversion
email.AddBccToHeader = true // Don't ignore Bcc
email.AllowEmptyAttachments = true // Don't error on empty attachments
email.UseProvidedAddress = true // Don't try to parse the email address
email.Encoding = mail.EncodingBase64 // Doing it to be safe for when we have eventMessage (newline issues)
email.AllowDuplicateAddress = true // More "correct" conversion
email.AddBccToHeader = true // Don't ignore Bcc
email.AllowEmptyAttachments = true // Don't error on empty attachments
email.UseProvidedAddress = true // Don't try to parse the email address
if data.GetFrom() != nil {
email.SetFrom(formatAddress(data.GetFrom().GetEmailAddress()))
@ -189,6 +282,24 @@ func FromJSON(ctx context.Context, body []byte) (string, error) {
}
}
switch data.(type) {
case *models.EventMessageResponse, *models.EventMessage:
// We can't handle this as of now, not enough information
// TODO: Fetch event object from graph when fetching email
case *models.CalendarSharingMessage:
// TODO: Parse out calendar sharing message
// https://github.com/alcionai/corso/issues/5041
case *models.EventMessageRequest:
cal, err := getICalData(ctx, data)
if err != nil {
return "", clues.Wrap(err, "getting ical attachment")
}
if len(cal) > 0 {
email.AddAlternative(mail.TextCalendar, cal)
}
}
if err = email.GetError(); err != nil {
return "", clues.WrapWC(ctx, err, "converting to eml")
}

View File

@ -1,11 +1,13 @@
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"
@ -15,6 +17,7 @@ import (
"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/tester"
"github.com/alcionai/corso/src/pkg/services/m365/api"
)
@ -188,3 +191,137 @@ func (suite *EMLUnitSuite) TestConvert_edge_cases() {
})
}
}
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)
}

View File

@ -0,0 +1,103 @@
{
"@odata.type": "#microsoft.graph.eventMessageRequest",
"@odata.etag": "W/\"CwAAABYAAABBFDg0JJk7TY1fmsJrh7tNAAFY+7zz\"",
"id": "AAMkAGJiZmE2NGU4LTQ4YjktNDI1Mi1iMWQzLTQ1MmMxODJkZmQyNABGAAAAAABFdiK7oifWRb4ADuqgSRcnBwBBFDg0JJk7TY1fmsJrh7tNAAAAAAEJAABBFDg0JJk7TY1fmsJrh7tNAAFZU-vfAAA=",
"createdDateTime": "2024-01-15T11:26:41Z",
"lastModifiedDateTime": "2024-01-15T11:26:43Z",
"changeKey": "CwAAABYAAABBFDg0JJk7TY1fmsJrh7tNAAFY+7zz",
"categories": [],
"receivedDateTime": "2024-01-15T11:26:41Z",
"sentDateTime": "2024-01-15T11:26:41Z",
"hasAttachments": false,
"internetMessageId": "<PH0PR04MB7285DDED30186B1D8E1BD2AABC6C2@PH0PR04MB7285.namprd04.prod.outlook.com>",
"subject": "Invitable event",
"bodyPreview": "How come the sun is hot?\r\n\r\n________________________________________________________________________________\r\nMicrosoft Teams meeting\r\nJoin on your computer, mobile app or room device\r\nClick here to join the meeting\r\nMeeting ID: 290 273 192 285\r\nPasscode:",
"importance": "normal",
"parentFolderId": "AQMkAGJiAGZhNjRlOC00OGI5LTQyNTItYjFkMy00NTJjMTgyZGZkMjQALgAAA0V2IruiJ9ZFvgAO6qBJFycBAEEUODQkmTtNjV_awmuHu00AAAIBCQAAAA==",
"conversationId": "AAQkAGJiZmE2NGU4LTQ4YjktNDI1Mi1iMWQzLTQ1MmMxODJkZmQyNAAQABoIUFzzByJMltJobtYSAJ0=",
"conversationIndex": "AdpHpbXXGghQXPMHIkyW0mhu1hIAnQ==",
"isDeliveryReceiptRequested": null,
"isReadReceiptRequested": false,
"isRead": true,
"isDraft": false,
"webLink": "https://outlook.office365.com/owa/?ItemID=AAMkAGJiZmE2NGU4LTQ4YjktNDI1Mi1iMWQzLTQ1MmMxODJkZmQyNABGAAAAAABFdiK7oifWRb4ADuqgSRcnBwBBFDg0JJk7TY1fmsJrh7tNAAAAAAEJAABBFDg0JJk7TY1fmsJrh7tNAAFZU%2FvfAAA%3D&exvsurl=1&viewmodel=ReadMessageItem",
"inferenceClassification": "focused",
"meetingMessageType": "meetingRequest",
"type": "seriesMaster",
"isOutOfDate": false,
"isAllDay": false,
"isDelegated": false,
"responseRequested": true,
"allowNewTimeProposals": null,
"meetingRequestType": "newMeetingRequest",
"body": {
"contentType": "html",
"content": "<html><head>\r\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"></head><body><div class=\"elementToProof\" style=\"font-family:Aptos,Aptos_EmbeddedFont,Aptos_MSFontService,Calibri,Helvetica,sans-serif; font-size:12pt; color:rgb(0,0,0)\">How come the sun is hot?<br></div><br><div style=\"width:100%\"><span style=\"white-space:nowrap; color:#5F5F5F; opacity:.36\">________________________________________________________________________________</span> </div><div class=\"me-email-text\" lang=\"en-US\" style=\"color:#252424; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\"><div style=\"margin-top:24px; margin-bottom:20px\"><span style=\"font-size:24px; color:#252424\">Microsoft Teams meeting</span> </div><div style=\"margin-bottom:20px\"><div style=\"margin-top:0px; margin-bottom:0px; font-weight:bold\"><span style=\"font-size:14px; color:#252424\">Join on your computer, mobile app or room device</span> </div><a class=\"me-email-headline\" href=\"https://teams.microsoft.com/l/meetup-join/19%3ameeting_OGM4MWVlYjUtMjllMi00ZjY5LWE5YjgtMTc4MjJhMWI1MjRl%40thread.v2/0?context=%7b%22Tid%22%3a%22fb8afbaa-e94c-4ea5-8a8a-24aff04d7874%22%2c%22Oid%22%3a%227ceb8e03-bdc5-4509-a136-457526165ec0%22%7d\" target=\"_blank\" rel=\"noreferrer noopener\" style=\"font-size:14px; font-family:'Segoe UI Semibold','Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif; text-decoration:underline; color:#6264a7\">Click here to join the meeting</a> </div><div style=\"margin-bottom:20px; margin-top:20px\"><div style=\"margin-bottom:4px\"><span data-tid=\"meeting-code\" style=\"font-size:14px; color:#252424\">Meeting ID: <span style=\"font-size:16px; color:#252424\">290 273 192 285</span> </span><br><span style=\"font-size:14px; color:#252424\">Passcode: </span><span style=\"font-size:16px; color:#252424\">CwEBTS </span><div style=\"font-size:14px\"><a class=\"me-email-link\" target=\"_blank\" href=\"https://www.microsoft.com/en-us/microsoft-teams/download-app\" rel=\"noreferrer noopener\" style=\"font-size:14px; text-decoration:underline; color:#6264a7; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\">Download Teams</a> | <a class=\"me-email-link\" target=\"_blank\" href=\"https://www.microsoft.com/microsoft-teams/join-a-meeting\" rel=\"noreferrer noopener\" style=\"font-size:14px; text-decoration:underline; color:#6264a7; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\">Join on the web</a></div></div></div><div style=\"margin-bottom:24px; margin-top:20px\"><a class=\"me-email-link\" target=\"_blank\" href=\"https://aka.ms/JoinTeamsMeeting\" rel=\"noreferrer noopener\" style=\"font-size:14px; text-decoration:underline; color:#6264a7; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\">Learn More</a> | <a class=\"me-email-link\" target=\"_blank\" href=\"https://teams.microsoft.com/meetingOptions/?organizerId=7ceb8e03-bdc5-4509-a136-457526165ec0&amp;tenantId=fb8afbaa-e94c-4ea5-8a8a-24aff04d7874&amp;threadId=19_meeting_OGM4MWVlYjUtMjllMi00ZjY5LWE5YjgtMTc4MjJhMWI1MjRl@thread.v2&amp;messageId=0&amp;language=en-US\" rel=\"noreferrer noopener\" style=\"font-size:14px; text-decoration:underline; color:#6264a7; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\">Meeting options</a> </div></div><div style=\"font-size:14px; margin-bottom:4px; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\"></div><div style=\"font-size:12px\"></div><div></div><div style=\"width:100%\"><span style=\"white-space:nowrap; color:#5F5F5F; opacity:.36\">________________________________________________________________________________</span> </div></body></html>"
},
"sender": {
"emailAddress": {
"name": "Johanna Lorenz",
"address": "JohannaL@10rqc2.onmicrosoft.com"
}
},
"from": {
"emailAddress": {
"name": "Johanna Lorenz",
"address": "JohannaL@10rqc2.onmicrosoft.com"
}
},
"toRecipients": [
{
"emailAddress": {
"name": "Faker 1",
"address": "fakeid1@provider.com"
}
}
],
"ccRecipients": [
{
"emailAddress": {
"name": "Faker 2",
"address": "fakeid2@provider.com"
}
}
],
"bccRecipients": [],
"replyTo": [],
"flag": {
"flagStatus": "notFlagged"
},
"startDateTime": {
"dateTime": "2024-01-22T02:30:00.0000000",
"timeZone": "UTC"
},
"endDateTime": {
"dateTime": "2024-01-22T03:00:00.0000000",
"timeZone": "UTC"
},
"location": {
"displayName": "Microsoft Teams Meeting",
"locationType": "default",
"uniqueIdType": "unknown"
},
"recurrence": {
"pattern": {
"type": "daily",
"interval": 1,
"month": 0,
"dayOfMonth": 0,
"firstDayOfWeek": "sunday",
"index": "first"
},
"range": {
"type": "endDate",
"startDate": "2024-01-21",
"endDate": "2024-01-24",
"recurrenceTimeZone": "tzone://Microsoft/Utc",
"numberOfOccurrences": 0
}
},
"previousLocation": null,
"previousStartDateTime": null,
"previousEndDateTime": null
}

View File

@ -0,0 +1,204 @@
{
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('JohannaL%4010rqc2.onmicrosoft.com')/messages(microsoft.graph.eventMessage/event())/$entity",
"@odata.type": "#microsoft.graph.eventMessageRequest",
"@odata.etag": "W/\"CwAAABYAAABBFDg0JJk7TY1fmsJrh7tNAAFY+7zz\"",
"id": "AAMkAGJiZmE2NGU4LTQ4YjktNDI1Mi1iMWQzLTQ1MmMxODJkZmQyNABGAAAAAABFdiK7oifWRb4ADuqgSRcnBwBBFDg0JJk7TY1fmsJrh7tNAAAAAAEJAABBFDg0JJk7TY1fmsJrh7tNAAFZU-vfAAA=",
"createdDateTime": "2024-01-15T11:26:41Z",
"lastModifiedDateTime": "2024-01-15T11:26:43Z",
"changeKey": "CwAAABYAAABBFDg0JJk7TY1fmsJrh7tNAAFY+7zz",
"categories": [],
"receivedDateTime": "2024-01-15T11:26:41Z",
"sentDateTime": "2024-01-15T11:26:41Z",
"hasAttachments": false,
"internetMessageId": "<PH0PR04MB7285DDED30186B1D8E1BD2AABC6C2@PH0PR04MB7285.namprd04.prod.outlook.com>",
"subject": "Invitable event",
"bodyPreview": "How come the sun is hot?\r\n\r\n________________________________________________________________________________\r\nMicrosoft Teams meeting\r\nJoin on your computer, mobile app or room device\r\nClick here to join the meeting\r\nMeeting ID: 290 273 192 285\r\nPasscode:",
"importance": "normal",
"parentFolderId": "AQMkAGJiAGZhNjRlOC00OGI5LTQyNTItYjFkMy00NTJjMTgyZGZkMjQALgAAA0V2IruiJ9ZFvgAO6qBJFycBAEEUODQkmTtNjV_awmuHu00AAAIBCQAAAA==",
"conversationId": "AAQkAGJiZmE2NGU4LTQ4YjktNDI1Mi1iMWQzLTQ1MmMxODJkZmQyNAAQABoIUFzzByJMltJobtYSAJ0=",
"conversationIndex": "AdpHpbXXGghQXPMHIkyW0mhu1hIAnQ==",
"isDeliveryReceiptRequested": null,
"isReadReceiptRequested": false,
"isRead": true,
"isDraft": false,
"webLink": "https://outlook.office365.com/owa/?ItemID=AAMkAGJiZmE2NGU4LTQ4YjktNDI1Mi1iMWQzLTQ1MmMxODJkZmQyNABGAAAAAABFdiK7oifWRb4ADuqgSRcnBwBBFDg0JJk7TY1fmsJrh7tNAAAAAAEJAABBFDg0JJk7TY1fmsJrh7tNAAFZU%2FvfAAA%3D&exvsurl=1&viewmodel=ReadMessageItem",
"inferenceClassification": "focused",
"meetingMessageType": "meetingRequest",
"type": "seriesMaster",
"isOutOfDate": false,
"isAllDay": false,
"isDelegated": false,
"responseRequested": true,
"allowNewTimeProposals": null,
"meetingRequestType": "newMeetingRequest",
"body": {
"contentType": "html",
"content": "<html><head>\r\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"></head><body><div class=\"elementToProof\" style=\"font-family:Aptos,Aptos_EmbeddedFont,Aptos_MSFontService,Calibri,Helvetica,sans-serif; font-size:12pt; color:rgb(0,0,0)\">How come the sun is hot?<br></div><br><div style=\"width:100%\"><span style=\"white-space:nowrap; color:#5F5F5F; opacity:.36\">________________________________________________________________________________</span> </div><div class=\"me-email-text\" lang=\"en-US\" style=\"color:#252424; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\"><div style=\"margin-top:24px; margin-bottom:20px\"><span style=\"font-size:24px; color:#252424\">Microsoft Teams meeting</span> </div><div style=\"margin-bottom:20px\"><div style=\"margin-top:0px; margin-bottom:0px; font-weight:bold\"><span style=\"font-size:14px; color:#252424\">Join on your computer, mobile app or room device</span> </div><a class=\"me-email-headline\" href=\"https://teams.microsoft.com/l/meetup-join/19%3ameeting_OGM4MWVlYjUtMjllMi00ZjY5LWE5YjgtMTc4MjJhMWI1MjRl%40thread.v2/0?context=%7b%22Tid%22%3a%22fb8afbaa-e94c-4ea5-8a8a-24aff04d7874%22%2c%22Oid%22%3a%227ceb8e03-bdc5-4509-a136-457526165ec0%22%7d\" target=\"_blank\" rel=\"noreferrer noopener\" style=\"font-size:14px; font-family:'Segoe UI Semibold','Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif; text-decoration:underline; color:#6264a7\">Click here to join the meeting</a> </div><div style=\"margin-bottom:20px; margin-top:20px\"><div style=\"margin-bottom:4px\"><span data-tid=\"meeting-code\" style=\"font-size:14px; color:#252424\">Meeting ID: <span style=\"font-size:16px; color:#252424\">290 273 192 285</span> </span><br><span style=\"font-size:14px; color:#252424\">Passcode: </span><span style=\"font-size:16px; color:#252424\">CwEBTS </span><div style=\"font-size:14px\"><a class=\"me-email-link\" target=\"_blank\" href=\"https://www.microsoft.com/en-us/microsoft-teams/download-app\" rel=\"noreferrer noopener\" style=\"font-size:14px; text-decoration:underline; color:#6264a7; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\">Download Teams</a> | <a class=\"me-email-link\" target=\"_blank\" href=\"https://www.microsoft.com/microsoft-teams/join-a-meeting\" rel=\"noreferrer noopener\" style=\"font-size:14px; text-decoration:underline; color:#6264a7; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\">Join on the web</a></div></div></div><div style=\"margin-bottom:24px; margin-top:20px\"><a class=\"me-email-link\" target=\"_blank\" href=\"https://aka.ms/JoinTeamsMeeting\" rel=\"noreferrer noopener\" style=\"font-size:14px; text-decoration:underline; color:#6264a7; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\">Learn More</a> | <a class=\"me-email-link\" target=\"_blank\" href=\"https://teams.microsoft.com/meetingOptions/?organizerId=7ceb8e03-bdc5-4509-a136-457526165ec0&amp;tenantId=fb8afbaa-e94c-4ea5-8a8a-24aff04d7874&amp;threadId=19_meeting_OGM4MWVlYjUtMjllMi00ZjY5LWE5YjgtMTc4MjJhMWI1MjRl@thread.v2&amp;messageId=0&amp;language=en-US\" rel=\"noreferrer noopener\" style=\"font-size:14px; text-decoration:underline; color:#6264a7; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\">Meeting options</a> </div></div><div style=\"font-size:14px; margin-bottom:4px; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\"></div><div style=\"font-size:12px\"></div><div></div><div style=\"width:100%\"><span style=\"white-space:nowrap; color:#5F5F5F; opacity:.36\">________________________________________________________________________________</span> </div></body></html>"
},
"sender": {
"emailAddress": {
"name": "Johanna Lorenz",
"address": "JohannaL@10rqc2.onmicrosoft.com"
}
},
"from": {
"emailAddress": {
"name": "Johanna Lorenz",
"address": "JohannaL@10rqc2.onmicrosoft.com"
}
},
"toRecipients": [
{
"emailAddress": {
"name": "Pradeep Gupta",
"address": "PradeepG@10rqc2.onmicrosoft.com"
}
}
],
"ccRecipients": [],
"bccRecipients": [],
"replyTo": [],
"flag": {
"flagStatus": "notFlagged"
},
"startDateTime": {
"dateTime": "2024-01-22T02:30:00.0000000",
"timeZone": "UTC"
},
"endDateTime": {
"dateTime": "2024-01-22T03:00:00.0000000",
"timeZone": "UTC"
},
"location": {
"displayName": "Microsoft Teams Meeting",
"locationType": "default",
"uniqueIdType": "unknown"
},
"recurrence": {
"pattern": {
"type": "daily",
"interval": 1,
"month": 0,
"dayOfMonth": 0,
"firstDayOfWeek": "sunday",
"index": "first"
},
"range": {
"type": "endDate",
"startDate": "2024-01-21",
"endDate": "2024-01-24",
"recurrenceTimeZone": "tzone://Microsoft/Utc",
"numberOfOccurrences": 0
}
},
"previousLocation": null,
"previousStartDateTime": null,
"previousEndDateTime": null,
"event@odata.associationLink": "https://graph.microsoft.com/v1.0/users('JohannaL@10rqc2.onmicrosoft.com')/events('AAMkAGJiZmE2NGU4LTQ4YjktNDI1Mi1iMWQzLTQ1MmMxODJkZmQyNABGAAAAAABFdiK7oifWRb4ADuqgSRcnBwBBFDg0JJk7TY1fmsJrh7tNAAAAAAENAABBFDg0JJk7TY1fmsJrh7tNAAFZU--pAAA=')/$ref",
"event@odata.navigationLink": "https://graph.microsoft.com/v1.0/users('JohannaL@10rqc2.onmicrosoft.com')/events('AAMkAGJiZmE2NGU4LTQ4YjktNDI1Mi1iMWQzLTQ1MmMxODJkZmQyNABGAAAAAABFdiK7oifWRb4ADuqgSRcnBwBBFDg0JJk7TY1fmsJrh7tNAAAAAAENAABBFDg0JJk7TY1fmsJrh7tNAAFZU--pAAA=')",
"event": {
"@odata.etag": "W/\"QRQ4NCSZO02NX5rCa4e7TQABWPwm+A==\"",
"id": "AAMkAGJiZmE2NGU4LTQ4YjktNDI1Mi1iMWQzLTQ1MmMxODJkZmQyNABGAAAAAABFdiK7oifWRb4ADuqgSRcnBwBBFDg0JJk7TY1fmsJrh7tNAAAAAAENAABBFDg0JJk7TY1fmsJrh7tNAAFZU--pAAA=",
"createdDateTime": "2024-01-15T11:26:39.1524133Z",
"lastModifiedDateTime": "2024-01-16T10:29:34.8704061Z",
"changeKey": "QRQ4NCSZO02NX5rCa4e7TQABWPwm+A==",
"categories": [],
"transactionId": "d19de894-1d85-dde1-ea5f-9332e850667b",
"originalStartTimeZone": "India Standard Time",
"originalEndTimeZone": "India Standard Time",
"iCalUId": "040000008200E00074C5B7101A82E008000000002757EEB4A547DA01000000000000000010000000CCC41D0213F00E489061EF756A0E6864",
"reminderMinutesBeforeStart": 15,
"isReminderOn": true,
"hasAttachments": false,
"subject": "Different title to test",
"bodyPreview": "How come the sun is hot?\r\n\r\n________________________________________________________________________________\r\nMicrosoft Teams meeting\r\nJoin on your computer, mobile app or room device\r\nClick here to join the meeting\r\nMeeting ID: 290 273 192 285\r\nPasscode:",
"importance": "normal",
"sensitivity": "normal",
"isAllDay": false,
"isCancelled": false,
"isOrganizer": true,
"responseRequested": true,
"seriesMasterId": null,
"showAs": "busy",
"type": "seriesMaster",
"webLink": "https://outlook.office365.com/owa/?itemid=AAMkAGJiZmE2NGU4LTQ4YjktNDI1Mi1iMWQzLTQ1MmMxODJkZmQyNABGAAAAAABFdiK7oifWRb4ADuqgSRcnBwBBFDg0JJk7TY1fmsJrh7tNAAAAAAENAABBFDg0JJk7TY1fmsJrh7tNAAFZU%2F%2FpAAA%3D&exvsurl=1&path=/calendar/item",
"onlineMeetingUrl": null,
"isOnlineMeeting": true,
"onlineMeetingProvider": "teamsForBusiness",
"allowNewTimeProposals": true,
"occurrenceId": null,
"isDraft": false,
"hideAttendees": false,
"responseStatus": {
"response": "organizer",
"time": "0001-01-01T00:00:00Z"
},
"body": {
"contentType": "html",
"content": "<html>\r\n<head>\r\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\r\n</head>\r\n<body>\r\n<div class=\"elementToProof\" style=\"font-family:Aptos,Aptos_EmbeddedFont,Aptos_MSFontService,Calibri,Helvetica,sans-serif; font-size:12pt; color:rgb(0,0,0)\">\r\nHow come the sun is hot?<br>\r\n</div>\r\n<br>\r\n<div style=\"width:100%\"><span style=\"white-space:nowrap; color:#5F5F5F; opacity:.36\">________________________________________________________________________________</span>\r\n</div>\r\n<div class=\"me-email-text\" lang=\"en-US\" style=\"color:#252424; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\">\r\n<div style=\"margin-top:24px; margin-bottom:20px\"><span style=\"font-size:24px; color:#252424\">Microsoft Teams meeting</span>\r\n</div>\r\n<div style=\"margin-bottom:20px\">\r\n<div style=\"margin-top:0px; margin-bottom:0px; font-weight:bold\"><span style=\"font-size:14px; color:#252424\">Join on your computer, mobile app or room device</span>\r\n</div>\r\n<a href=\"https://teams.microsoft.com/l/meetup-join/19%3ameeting_OGM4MWVlYjUtMjllMi00ZjY5LWE5YjgtMTc4MjJhMWI1MjRl%40thread.v2/0?context=%7b%22Tid%22%3a%22fb8afbaa-e94c-4ea5-8a8a-24aff04d7874%22%2c%22Oid%22%3a%227ceb8e03-bdc5-4509-a136-457526165ec0%22%7d\" class=\"me-email-headline\" style=\"font-size:14px; font-family:'Segoe UI Semibold','Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif; text-decoration:underline; color:#6264a7\">Click\r\n here to join the meeting</a> </div>\r\n<div style=\"margin-bottom:20px; margin-top:20px\">\r\n<div style=\"margin-bottom:4px\"><span data-tid=\"meeting-code\" style=\"font-size:14px; color:#252424\">Meeting ID:\r\n<span style=\"font-size:16px; color:#252424\">290 273 192 285</span> </span><br>\r\n<span style=\"font-size:14px; color:#252424\">Passcode: </span><span style=\"font-size:16px; color:#252424\">CwEBTS\r\n</span>\r\n<div style=\"font-size:14px\"><a href=\"https://www.microsoft.com/en-us/microsoft-teams/download-app\" class=\"me-email-link\" style=\"font-size:14px; text-decoration:underline; color:#6264a7; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\">Download\r\n Teams</a> | <a href=\"https://www.microsoft.com/microsoft-teams/join-a-meeting\" class=\"me-email-link\" style=\"font-size:14px; text-decoration:underline; color:#6264a7; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\">\r\nJoin on the web</a></div>\r\n</div>\r\n</div>\r\n<div style=\"margin-bottom:24px; margin-top:20px\"><a href=\"https://aka.ms/JoinTeamsMeeting\" class=\"me-email-link\" style=\"font-size:14px; text-decoration:underline; color:#6264a7; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\">Learn More</a>\r\n | <a href=\"https://teams.microsoft.com/meetingOptions/?organizerId=7ceb8e03-bdc5-4509-a136-457526165ec0&amp;tenantId=fb8afbaa-e94c-4ea5-8a8a-24aff04d7874&amp;threadId=19_meeting_OGM4MWVlYjUtMjllMi00ZjY5LWE5YjgtMTc4MjJhMWI1MjRl@thread.v2&amp;messageId=0&amp;language=en-US\" class=\"me-email-link\" style=\"font-size:14px; text-decoration:underline; color:#6264a7; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\">\r\nMeeting options</a> </div>\r\n</div>\r\n<div style=\"font-size:14px; margin-bottom:4px; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\">\r\n</div>\r\n<div style=\"font-size:12px\"></div>\r\n<div></div>\r\n<div style=\"width:100%\"><span style=\"white-space:nowrap; color:#5F5F5F; opacity:.36\">________________________________________________________________________________</span>\r\n</div>\r\n</body>\r\n</html>\r\n"
},
"start": {
"dateTime": "2024-01-22T02:30:00.0000000",
"timeZone": "UTC"
},
"end": {
"dateTime": "2024-01-22T03:00:00.0000000",
"timeZone": "UTC"
},
"location": {
"displayName": "Microsoft Teams Meeting",
"locationType": "default",
"uniqueId": "Microsoft Teams Meeting",
"uniqueIdType": "private"
},
"locations": [
{
"displayName": "Microsoft Teams Meeting",
"locationType": "default",
"uniqueId": "Microsoft Teams Meeting",
"uniqueIdType": "private"
}
],
"recurrence": {
"pattern": {
"type": "daily",
"interval": 1,
"month": 0,
"dayOfMonth": 0,
"firstDayOfWeek": "sunday",
"index": "first"
},
"range": {
"type": "endDate",
"startDate": "2024-01-22",
"endDate": "2024-01-25",
"recurrenceTimeZone": "India Standard Time",
"numberOfOccurrences": 0
}
},
"attendees": [
{
"type": "required",
"status": {
"response": "none",
"time": "0001-01-01T00:00:00Z"
},
"emailAddress": {
"name": "Pradeep Gupta",
"address": "PradeepG@10rqc2.onmicrosoft.com"
}
}
],
"organizer": {
"emailAddress": {
"name": "Johanna Lorenz",
"address": "JohannaL@10rqc2.onmicrosoft.com"
}
},
"onlineMeeting": {
"joinUrl": "https://teams.microsoft.com/l/meetup-join/19%3ameeting_OGM4MWVlYjUtMjllMi00ZjY5LWE5YjgtMTc4MjJhMWI1MjRl%40thread.v2/0?context=%7b%22Tid%22%3a%22fb8afbaa-e94c-4ea5-8a8a-24aff04d7874%22%2c%22Oid%22%3a%227ceb8e03-bdc5-4509-a136-457526165ec0%22%7d"
},
"calendar@odata.associationLink": "https://graph.microsoft.com/v1.0/users('JohannaL@10rqc2.onmicrosoft.com')/calendars('AQMkAGJiAGZhNjRlOC00OGI5LTQyNTItYjFkMy00NTJjMTgyZGZkMjQALgAAA0V2IruiJ9ZFvgAO6qBJFycBAEEUODQkmTtNjV_awmuHu00AAAIBDQAAAA==')/$ref",
"calendar@odata.navigationLink": "https://graph.microsoft.com/v1.0/users('JohannaL@10rqc2.onmicrosoft.com')/calendars('AQMkAGJiAGZhNjRlOC00OGI5LTQyNTItYjFkMy00NTJjMTgyZGZkMjQALgAAA0V2IruiJ9ZFvgAO6qBJFycBAEEUODQkmTtNjV_awmuHu00AAAIBDQAAAA==')"
}
}

View File

@ -4,3 +4,9 @@ import _ "embed"
//go:embed email-with-attachments.json
var EmailWithAttachments string
//go:embed email-with-event-info.json
var EmailWithEventInfo string
//go:embed email-with-event-object.json
var EmailWithEventObject string

View File

@ -30,8 +30,8 @@ import (
// TODO locations: https://github.com/alcionai/corso/issues/5003
const (
iCalDateTimeFormat = "20060102T150405Z"
iCalDateFormat = "20060102"
ICalDateTimeFormat = "20060102T150405Z"
ICalDateFormat = "20060102"
)
func keyValues(key, value string) *ics.KeyValues {
@ -71,7 +71,7 @@ func getLocationString(location models.Locationable) string {
return strings.Join(nonEmpty, ", ")
}
func getUTCTime(ts, tz string) (time.Time, error) {
func GetUTCTime(ts, tz string) (time.Time, error) {
// Timezone is always converted to UTC. This is the easiest way to
// ensure we have the correct time as the .ics file expects the same
// timezone everywhere according to the spec.
@ -179,14 +179,14 @@ func getRecurrencePattern(
// the resolution we need
parsedTime = parsedTime.Add(24*time.Hour - 1*time.Second)
endTime, err := getUTCTime(
endTime, err := GetUTCTime(
parsedTime.Format(string(dttm.M365DateTimeTimeZone)),
ptr.Val(rrange.GetRecurrenceTimeZone()))
if err != nil {
return "", clues.WrapWC(ctx, err, "parsing end time")
}
recurComponents = append(recurComponents, "UNTIL="+endTime.Format(iCalDateTimeFormat))
recurComponents = append(recurComponents, "UNTIL="+endTime.Format(ICalDateTimeFormat))
}
case models.NOEND_RECURRENCERANGETYPE:
// Nothing to do
@ -208,13 +208,17 @@ func FromJSON(ctx context.Context, body []byte) (string, error) {
With("body_len", len(body))
}
return FromEventable(ctx, event)
}
func FromEventable(ctx context.Context, event models.Eventable) (string, error) {
cal := ics.NewCalendar()
cal.SetProductId("-//Alcion//Corso") // Does this have to be customizable?
id := ptr.Val(event.GetId())
iCalEvent := cal.AddEvent(id)
err = updateEventProperties(ctx, event, iCalEvent)
err := updateEventProperties(ctx, event, iCalEvent)
if err != nil {
return "", clues.Wrap(err, "updating event properties")
}
@ -245,7 +249,7 @@ func FromJSON(ctx context.Context, body []byte) (string, error) {
exICalEvent := cal.AddEvent(id)
start := exception.GetOriginalStart() // will always be in UTC
exICalEvent.AddProperty(ics.ComponentProperty(ics.PropertyRecurrenceId), start.Format(iCalDateTimeFormat))
exICalEvent.AddProperty(ics.ComponentProperty(ics.PropertyRecurrenceId), start.Format(ICalDateTimeFormat))
err = updateEventProperties(ctx, exception, exICalEvent)
if err != nil {
@ -285,7 +289,7 @@ func updateEventProperties(ctx context.Context, event models.Eventable, iCalEven
startTimezone := event.GetStart().GetTimeZone()
if startString != nil {
start, err := getUTCTime(ptr.Val(startString), ptr.Val(startTimezone))
start, err := GetUTCTime(ptr.Val(startString), ptr.Val(startTimezone))
if err != nil {
return clues.WrapWC(ctx, err, "parsing start time")
}
@ -302,7 +306,7 @@ func updateEventProperties(ctx context.Context, event models.Eventable, iCalEven
endTimezone := event.GetEnd().GetTimeZone()
if endString != nil {
end, err := getUTCTime(ptr.Val(endString), ptr.Val(endTimezone))
end, err := GetUTCTime(ptr.Val(endString), ptr.Val(endTimezone))
if err != nil {
return clues.WrapWC(ctx, err, "parsing end time")
}
@ -577,7 +581,7 @@ func updateEventProperties(ctx context.Context, event models.Eventable, iCalEven
dateStrings := []string{}
for _, date := range cancelledDates {
dateStrings = append(dateStrings, date.Format(iCalDateFormat))
dateStrings = append(dateStrings, date.Format(ICalDateFormat))
}
if len(dateStrings) > 0 {
@ -598,7 +602,7 @@ func getCancelledDates(ctx context.Context, event models.Eventable) ([]time.Time
for _, ds := range dateStrings {
// the data just contains date and no time which seems to work
start, err := getUTCTime(ds, tz)
start, err := GetUTCTime(ds, tz)
if err != nil {
return nil, clues.WrapWC(ctx, err, "parsing cancelled event date")
}

View File

@ -156,7 +156,7 @@ func (suite *ICSUnitSuite) TestGetUTCTime() {
for _, tt := range table {
suite.Run(tt.name, func() {
t, err := getUTCTime(tt.timestamp, tt.timezone)
t, err := GetUTCTime(tt.timestamp, tt.timezone)
tt.errCheck(suite.T(), err)
if !tt.time.Equal(time.Time{}) {