Events export as ICS (#4958)
There are a few more pending items. I've added TODO entries for them. The PR was getting pretty big and so I thought I would address them in a followup PR. --- #### Does this PR need a docs update or release note? - [ ] ✅ Yes, it's included - [ ] 🕐 Yes, but in a later PR - [ ] ⛔ No #### Type of change <!--- Please check the type of change your PR introduces: ---> - [ ] 🌻 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/3890 #### Test Plan <!-- How will this be tested prior to merging.--> - [ ] 💪 Manual - [ ] ⚡ Unit test - [ ] 💚 E2E
This commit is contained in:
parent
c87533a266
commit
b49c124512
@ -7,6 +7,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/alcionai/corso/src/internal/converters/eml"
|
"github.com/alcionai/corso/src/internal/converters/eml"
|
||||||
|
"github.com/alcionai/corso/src/internal/converters/ics"
|
||||||
"github.com/alcionai/corso/src/internal/converters/vcf"
|
"github.com/alcionai/corso/src/internal/converters/vcf"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -40,6 +41,11 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
case "ics":
|
||||||
|
out, err = ics.FromJSON(context.Background(), body)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
log.Fatal("Unknown target format", to)
|
log.Fatal("Unknown target format", to)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -59,6 +59,7 @@ require (
|
|||||||
github.com/VividCortex/ewma v1.2.0 // indirect
|
github.com/VividCortex/ewma v1.2.0 // indirect
|
||||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
|
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
|
||||||
github.com/andybalholm/brotli v1.0.6 // indirect
|
github.com/andybalholm/brotli v1.0.6 // indirect
|
||||||
|
github.com/arran4/golang-ical v0.2.3 // indirect
|
||||||
github.com/aws/aws-sdk-go v1.48.6 // indirect
|
github.com/aws/aws-sdk-go v1.48.6 // indirect
|
||||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
|
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
|
||||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
|
|||||||
@ -33,6 +33,8 @@ github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sx
|
|||||||
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA=
|
github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA=
|
||||||
github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
|
github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
|
||||||
|
github.com/arran4/golang-ical v0.2.3 h1:C4Vj7+BjJBIrAJhHgi6Ku+XUkQVugRq4re5Cqj5QVdE=
|
||||||
|
github.com/arran4/golang-ical v0.2.3/go.mod h1:RqMuPGmwRRwjkb07hmm+JBqcWa1vF1LvVmPtSZN2OhQ=
|
||||||
github.com/aws/aws-sdk-go v1.48.6 h1:hnL/TE3eRigirDLrdRE9AWE1ALZSVLAsC4wK8TGsMqk=
|
github.com/aws/aws-sdk-go v1.48.6 h1:hnL/TE3eRigirDLrdRE9AWE1ALZSVLAsC4wK8TGsMqk=
|
||||||
github.com/aws/aws-sdk-go v1.48.6/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
|
github.com/aws/aws-sdk-go v1.48.6/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
|
||||||
github.com/aws/aws-xray-sdk-go v1.8.3 h1:S8GdgVncBRhzbNnNUgTPwhEqhwt2alES/9rLASyhxjU=
|
github.com/aws/aws-xray-sdk-go v1.8.3 h1:S8GdgVncBRhzbNnNUgTPwhEqhwt2alES/9rLASyhxjU=
|
||||||
@ -57,6 +59,7 @@ github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp
|
|||||||
github.com/cjlapao/common-go v0.0.39 h1:bAAUrj2B9v0kMzbAOhzjSmiyDy+rd56r2sy7oEiQLlA=
|
github.com/cjlapao/common-go v0.0.39 h1:bAAUrj2B9v0kMzbAOhzjSmiyDy+rd56r2sy7oEiQLlA=
|
||||||
github.com/cjlapao/common-go v0.0.39/go.mod h1:M3dzazLjTjEtZJbbxoA5ZDiGCiHmpwqW9l4UWaddwOA=
|
github.com/cjlapao/common-go v0.0.39/go.mod h1:M3dzazLjTjEtZJbbxoA5ZDiGCiHmpwqW9l4UWaddwOA=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE=
|
github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE=
|
||||||
github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec=
|
github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@ -230,6 +233,7 @@ github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0
|
|||||||
github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM=
|
github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM=
|
||||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
|
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
|
||||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
|
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||||
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
|
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
|
||||||
@ -413,6 +417,7 @@ google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
|
|||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||||
@ -425,6 +430,7 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
|
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
|
||||||
|
|||||||
168
src/internal/converters/ics/consts.go
Normal file
168
src/internal/converters/ics/consts.go
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
package ics
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Map from Graph API recurrence type to iCal recurrence type
|
||||||
|
GraphToICalIndex = map[string]int{
|
||||||
|
"first": 1,
|
||||||
|
"second": 2,
|
||||||
|
"third": 3,
|
||||||
|
"fourth": 4,
|
||||||
|
"last": -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map from Graph API day of week representation to iCal day of week representation
|
||||||
|
GraphToICalDOW = map[string]string{
|
||||||
|
"sunday": "SU",
|
||||||
|
"monday": "MO",
|
||||||
|
"tuesday": "TU",
|
||||||
|
"wednesday": "WE",
|
||||||
|
"thursday": "TH",
|
||||||
|
"friday": "FR",
|
||||||
|
"saturday": "SA",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Map from Window time zone to TZ database time zone
|
||||||
|
// https://github.com/closeio/sync-engine/blob/1ce0e1ad0104a2ab2479da09b073c86f4feee5f9/inbox/events/timezones.py#L6
|
||||||
|
var GraphTimeZoneToTZ = map[string]string{
|
||||||
|
"AUS Central Standard Time": "Australia/Darwin",
|
||||||
|
"AUS Eastern Standard Time": "Australia/Sydney",
|
||||||
|
"Afghanistan Standard Time": "Asia/Kabul",
|
||||||
|
"Alaskan Standard Time": "America/Anchorage",
|
||||||
|
"Aleutian Standard Time": "America/Adak",
|
||||||
|
"Altai Standard Time": "Asia/Barnaul",
|
||||||
|
"Arab Standard Time": "Asia/Riyadh",
|
||||||
|
"Arabian Standard Time": "Asia/Dubai",
|
||||||
|
"Arabic Standard Time": "Asia/Baghdad",
|
||||||
|
"Argentina Standard Time": "America/Buenos_Aires",
|
||||||
|
"Astrakhan Standard Time": "Europe/Astrakhan",
|
||||||
|
"Atlantic Standard Time": "America/Halifax",
|
||||||
|
"Aus Central W. Standard Time": "Australia/Eucla",
|
||||||
|
"Azerbaijan Standard Time": "Asia/Baku",
|
||||||
|
"Azores Standard Time": "Atlantic/Azores",
|
||||||
|
"Bahia Standard Time": "America/Bahia",
|
||||||
|
"Bangladesh Standard Time": "Asia/Dhaka",
|
||||||
|
"Belarus Standard Time": "Europe/Minsk",
|
||||||
|
"Bougainville Standard Time": "Pacific/Bougainville",
|
||||||
|
"Canada Central Standard Time": "America/Regina",
|
||||||
|
"Cape Verde Standard Time": "Atlantic/Cape_Verde",
|
||||||
|
"Caucasus Standard Time": "Asia/Yerevan",
|
||||||
|
"Cen. Australia Standard Time": "Australia/Adelaide",
|
||||||
|
"Central America Standard Time": "America/Guatemala",
|
||||||
|
"Central Asia Standard Time": "Asia/Almaty",
|
||||||
|
"Central Brazilian Standard Time": "America/Cuiaba",
|
||||||
|
"Central Europe Standard Time": "Europe/Budapest",
|
||||||
|
"Central European Standard Time": "Europe/Warsaw",
|
||||||
|
"Central Pacific Standard Time": "Pacific/Guadalcanal",
|
||||||
|
"Central Standard Time": "America/Chicago",
|
||||||
|
"Central Standard Time (Mexico)": "America/Mexico_City",
|
||||||
|
"Chatham Islands Standard Time": "Pacific/Chatham",
|
||||||
|
"China Standard Time": "Asia/Shanghai",
|
||||||
|
"Cuba Standard Time": "America/Havana",
|
||||||
|
"Dateline Standard Time": "Etc/GMT+12",
|
||||||
|
"E. Africa Standard Time": "Africa/Nairobi",
|
||||||
|
"E. Australia Standard Time": "Australia/Brisbane",
|
||||||
|
"E. Europe Standard Time": "Europe/Chisinau",
|
||||||
|
"E. South America Standard Time": "America/Sao_Paulo",
|
||||||
|
"Easter Island Standard Time": "Pacific/Easter",
|
||||||
|
"Eastern Standard Time": "America/New_York",
|
||||||
|
"Eastern Standard Time (Mexico)": "America/Cancun",
|
||||||
|
"Egypt Standard Time": "Africa/Cairo",
|
||||||
|
"Ekaterinburg Standard Time": "Asia/Yekaterinburg",
|
||||||
|
"FLE Standard Time": "Europe/Kiev",
|
||||||
|
"Fiji Standard Time": "Pacific/Fiji",
|
||||||
|
"GMT Standard Time": "Europe/London",
|
||||||
|
"GTB Standard Time": "Europe/Bucharest",
|
||||||
|
"Georgian Standard Time": "Asia/Tbilisi",
|
||||||
|
"Greenland Standard Time": "America/Godthab",
|
||||||
|
"Greenwich Standard Time": "Atlantic/Reykjavik",
|
||||||
|
"Haiti Standard Time": "America/Port-au-Prince",
|
||||||
|
"Hawaiian Standard Time": "Pacific/Honolulu",
|
||||||
|
"India Standard Time": "Asia/Calcutta",
|
||||||
|
"Iran Standard Time": "Asia/Tehran",
|
||||||
|
"Israel Standard Time": "Asia/Jerusalem",
|
||||||
|
"Jordan Standard Time": "Asia/Amman",
|
||||||
|
"Kaliningrad Standard Time": "Europe/Kaliningrad",
|
||||||
|
"Korea Standard Time": "Asia/Seoul",
|
||||||
|
"Libya Standard Time": "Africa/Tripoli",
|
||||||
|
"Line Islands Standard Time": "Pacific/Kiritimati",
|
||||||
|
"Lord Howe Standard Time": "Australia/Lord_Howe",
|
||||||
|
"Magadan Standard Time": "Asia/Magadan",
|
||||||
|
"Magallanes Standard Time": "America/Punta_Arenas",
|
||||||
|
"Marquesas Standard Time": "Pacific/Marquesas",
|
||||||
|
"Mauritius Standard Time": "Indian/Mauritius",
|
||||||
|
"Middle East Standard Time": "Asia/Beirut",
|
||||||
|
"Montevideo Standard Time": "America/Montevideo",
|
||||||
|
"Morocco Standard Time": "Africa/Casablanca",
|
||||||
|
"Mountain Standard Time": "America/Denver",
|
||||||
|
"Mountain Standard Time (Mexico)": "America/Chihuahua",
|
||||||
|
"Myanmar Standard Time": "Asia/Rangoon",
|
||||||
|
"N. Central Asia Standard Time": "Asia/Novosibirsk",
|
||||||
|
"Namibia Standard Time": "Africa/Windhoek",
|
||||||
|
"Nepal Standard Time": "Asia/Katmandu",
|
||||||
|
"New Zealand Standard Time": "Pacific/Auckland",
|
||||||
|
"Newfoundland Standard Time": "America/St_Johns",
|
||||||
|
"Norfolk Standard Time": "Pacific/Norfolk",
|
||||||
|
"North Asia East Standard Time": "Asia/Irkutsk",
|
||||||
|
"North Asia Standard Time": "Asia/Krasnoyarsk",
|
||||||
|
"North Korea Standard Time": "Asia/Pyongyang",
|
||||||
|
"Omsk Standard Time": "Asia/Omsk",
|
||||||
|
"Pacific SA Standard Time": "America/Santiago",
|
||||||
|
"Pacific Standard Time": "America/Los_Angeles",
|
||||||
|
"Pacific Standard Time (Mexico)": "America/Tijuana",
|
||||||
|
"Pakistan Standard Time": "Asia/Karachi",
|
||||||
|
"Paraguay Standard Time": "America/Asuncion",
|
||||||
|
"Qyzylorda Standard Time": "Asia/Qyzylorda",
|
||||||
|
"Romance Standard Time": "Europe/Paris",
|
||||||
|
"Russia Time Zone 10": "Asia/Srednekolymsk",
|
||||||
|
"Russia Time Zone 11": "Asia/Kamchatka",
|
||||||
|
"Russia Time Zone 3": "Europe/Samara",
|
||||||
|
"Russian Standard Time": "Europe/Moscow",
|
||||||
|
"SA Eastern Standard Time": "America/Cayenne",
|
||||||
|
"SA Pacific Standard Time": "America/Bogota",
|
||||||
|
"SA Western Standard Time": "America/La_Paz",
|
||||||
|
"SE Asia Standard Time": "Asia/Bangkok",
|
||||||
|
"Saint Pierre Standard Time": "America/Miquelon",
|
||||||
|
"Sakhalin Standard Time": "Asia/Sakhalin",
|
||||||
|
"Samoa Standard Time": "Pacific/Apia",
|
||||||
|
"Sao Tome Standard Time": "Africa/Sao_Tome",
|
||||||
|
"Saratov Standard Time": "Europe/Saratov",
|
||||||
|
"Singapore Standard Time": "Asia/Singapore",
|
||||||
|
"South Africa Standard Time": "Africa/Johannesburg",
|
||||||
|
"South Sudan Standard Time": "Africa/Juba",
|
||||||
|
"Sri Lanka Standard Time": "Asia/Colombo",
|
||||||
|
"Sudan Standard Time": "Africa/Khartoum",
|
||||||
|
"Syria Standard Time": "Asia/Damascus",
|
||||||
|
"Taipei Standard Time": "Asia/Taipei",
|
||||||
|
"Tasmania Standard Time": "Australia/Hobart",
|
||||||
|
"Tocantins Standard Time": "America/Araguaina",
|
||||||
|
"Tokyo Standard Time": "Asia/Tokyo",
|
||||||
|
"Tomsk Standard Time": "Asia/Tomsk",
|
||||||
|
"Tonga Standard Time": "Pacific/Tongatapu",
|
||||||
|
"Transbaikal Standard Time": "Asia/Chita",
|
||||||
|
"Turkey Standard Time": "Europe/Istanbul",
|
||||||
|
"Turks And Caicos Standard Time": "America/Grand_Turk",
|
||||||
|
"US Eastern Standard Time": "America/Indianapolis",
|
||||||
|
"US Mountain Standard Time": "America/Phoenix",
|
||||||
|
"UTC": "Etc/UTC",
|
||||||
|
"UTC+12": "Etc/GMT-12",
|
||||||
|
"UTC+13": "Etc/GMT-13",
|
||||||
|
"UTC-02": "Etc/GMT+2",
|
||||||
|
"UTC-08": "Etc/GMT+8",
|
||||||
|
"UTC-09": "Etc/GMT+9",
|
||||||
|
"UTC-11": "Etc/GMT+11",
|
||||||
|
"Ulaanbaatar Standard Time": "Asia/Ulaanbaatar",
|
||||||
|
"Venezuela Standard Time": "America/Caracas",
|
||||||
|
"Vladivostok Standard Time": "Asia/Vladivostok",
|
||||||
|
"Volgograd Standard Time": "Europe/Volgograd",
|
||||||
|
"W. Australia Standard Time": "Australia/Perth",
|
||||||
|
"W. Central Africa Standard Time": "Africa/Lagos",
|
||||||
|
"W. Europe Standard Time": "Europe/Berlin",
|
||||||
|
"W. Mongolia Standard Time": "Asia/Hovd",
|
||||||
|
"West Asia Standard Time": "Asia/Tashkent",
|
||||||
|
"West Bank Standard Time": "Asia/Hebron",
|
||||||
|
"West Pacific Standard Time": "Pacific/Port_Moresby",
|
||||||
|
"Yakutsk Standard Time": "Asia/Yakutsk",
|
||||||
|
"Yukon Standard Time": "America/Whitehorse",
|
||||||
|
"tzone://Microsoft/Utc": "Etc/UTC",
|
||||||
|
}
|
||||||
447
src/internal/converters/ics/ics.go
Normal file
447
src/internal/converters/ics/ics.go
Normal file
@ -0,0 +1,447 @@
|
|||||||
|
package ics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
ics "github.com/arran4/golang-ical"
|
||||||
|
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/internal/common/ptr"
|
||||||
|
"github.com/alcionai/corso/src/internal/common/str"
|
||||||
|
"github.com/alcionai/corso/src/pkg/dttm"
|
||||||
|
"github.com/alcionai/corso/src/pkg/services/m365/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This package is used to convert json response from graph to ics
|
||||||
|
// Ref: https://icalendar.org/
|
||||||
|
// Ref: https://www.rfc-editor.org/rfc/rfc5545
|
||||||
|
// Ref: https://learn.microsoft.com/en-us/graph/api/resources/event?view=graph-rest-1.0
|
||||||
|
|
||||||
|
// TODO: Items not handled
|
||||||
|
// locations (different from location)
|
||||||
|
// exceptions and modifications
|
||||||
|
|
||||||
|
// Field in the backed up data that we cannot handle
|
||||||
|
// allowNewTimeProposals, hideAttendees, importance, isOnlineMeeting,
|
||||||
|
// isOrganizer, isReminderOn, onlineMeeting, onlineMeetingProvider,
|
||||||
|
// onlineMeetingUrl, originalEndTimeZone, originalStart,
|
||||||
|
// originalStartTimeZone, reminderMinutesBeforeStart, responseRequested,
|
||||||
|
// responseStatus, sensitivity
|
||||||
|
|
||||||
|
func keyValues(key, value string) *ics.KeyValues {
|
||||||
|
return &ics.KeyValues{
|
||||||
|
Key: key,
|
||||||
|
Value: []string{value},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLocationString(location models.Locationable) string {
|
||||||
|
if location == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
dn := ptr.Val(location.GetDisplayName())
|
||||||
|
segments := []string{dn}
|
||||||
|
|
||||||
|
// TODO: Handle different location types
|
||||||
|
addr := location.GetAddress()
|
||||||
|
if addr != nil {
|
||||||
|
street := ptr.Val(addr.GetStreet())
|
||||||
|
city := ptr.Val(addr.GetCity())
|
||||||
|
state := ptr.Val(addr.GetState())
|
||||||
|
country := ptr.Val(addr.GetCountryOrRegion())
|
||||||
|
postal := ptr.Val(addr.GetPostalCode())
|
||||||
|
|
||||||
|
segments = append(segments, street, city, state, country, postal)
|
||||||
|
}
|
||||||
|
|
||||||
|
nonEmpty := []string{}
|
||||||
|
|
||||||
|
for _, seg := range segments {
|
||||||
|
if len(seg) > 0 {
|
||||||
|
nonEmpty = append(nonEmpty, seg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(nonEmpty, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
it, err := dttm.ParseTime(ts)
|
||||||
|
if err != nil {
|
||||||
|
return time.Now(), clues.Wrap(err, "parsing time")
|
||||||
|
}
|
||||||
|
|
||||||
|
timezone, ok := GraphTimeZoneToTZ[tz]
|
||||||
|
if !ok {
|
||||||
|
return it, clues.New("unknown timezone")
|
||||||
|
}
|
||||||
|
|
||||||
|
loc, err := time.LoadLocation(timezone)
|
||||||
|
if err != nil {
|
||||||
|
return time.Now(), clues.Wrap(err, "loading timezone")
|
||||||
|
}
|
||||||
|
|
||||||
|
// embed timezone
|
||||||
|
locTime := time.Date(it.Year(), it.Month(), it.Day(), it.Hour(), it.Minute(), it.Second(), 0, loc)
|
||||||
|
|
||||||
|
return locTime.UTC(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc5545#section-3.8.5.3
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc5545#section-3.3.10
|
||||||
|
// https://learn.microsoft.com/en-us/graph/api/resources/patternedrecurrence?view=graph-rest-1.0
|
||||||
|
// Ref: https://github.com/closeio/sync-engine/pull/381/files
|
||||||
|
func getRecurrencePattern(
|
||||||
|
ctx context.Context,
|
||||||
|
recurrence models.PatternedRecurrenceable,
|
||||||
|
) (string, error) {
|
||||||
|
recurComponents := []string{}
|
||||||
|
pat := recurrence.GetPattern()
|
||||||
|
|
||||||
|
freq := pat.GetTypeEscaped()
|
||||||
|
if freq != nil {
|
||||||
|
switch *freq {
|
||||||
|
case models.DAILY_RECURRENCEPATTERNTYPE:
|
||||||
|
recurComponents = append(recurComponents, "FREQ=DAILY")
|
||||||
|
case models.WEEKLY_RECURRENCEPATTERNTYPE:
|
||||||
|
recurComponents = append(recurComponents, "FREQ=WEEKLY")
|
||||||
|
case models.ABSOLUTEMONTHLY_RECURRENCEPATTERNTYPE, models.RELATIVEMONTHLY_RECURRENCEPATTERNTYPE:
|
||||||
|
recurComponents = append(recurComponents, "FREQ=MONTHLY")
|
||||||
|
case models.ABSOLUTEYEARLY_RECURRENCEPATTERNTYPE, models.RELATIVEYEARLY_RECURRENCEPATTERNTYPE:
|
||||||
|
recurComponents = append(recurComponents, "FREQ=YEARLY")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interval := pat.GetInterval()
|
||||||
|
if interval != nil {
|
||||||
|
recurComponents = append(recurComponents, "INTERVAL="+fmt.Sprint(ptr.Val(interval)))
|
||||||
|
}
|
||||||
|
|
||||||
|
month := ptr.Val(pat.GetMonth())
|
||||||
|
if month > 0 {
|
||||||
|
recurComponents = append(recurComponents, "BYMONTH="+fmt.Sprint(month))
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is required if absoluteMonthly or absoluteYearly
|
||||||
|
day := ptr.Val(pat.GetDayOfMonth())
|
||||||
|
if day > 0 {
|
||||||
|
recurComponents = append(recurComponents, "BYMONTHDAY="+fmt.Sprint(day))
|
||||||
|
}
|
||||||
|
|
||||||
|
dow := pat.GetDaysOfWeek()
|
||||||
|
if dow != nil {
|
||||||
|
dowComponents := []string{}
|
||||||
|
|
||||||
|
for _, day := range dow {
|
||||||
|
icalday, ok := GraphToICalDOW[day.String()]
|
||||||
|
if !ok {
|
||||||
|
return "", clues.NewWC(ctx, "unknown day of week").With("day", day.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
dowComponents = append(dowComponents, icalday)
|
||||||
|
}
|
||||||
|
|
||||||
|
index := pat.GetIndex()
|
||||||
|
prefix := ""
|
||||||
|
|
||||||
|
if index != nil &&
|
||||||
|
(ptr.Val(freq) == models.RELATIVEMONTHLY_RECURRENCEPATTERNTYPE ||
|
||||||
|
ptr.Val(freq) == models.RELATIVEYEARLY_RECURRENCEPATTERNTYPE) {
|
||||||
|
prefix = fmt.Sprint(GraphToICalIndex[index.String()])
|
||||||
|
}
|
||||||
|
|
||||||
|
recurComponents = append(recurComponents, "BYDAY="+prefix+strings.Join(dowComponents, ","))
|
||||||
|
}
|
||||||
|
|
||||||
|
rrange := recurrence.GetRangeEscaped()
|
||||||
|
if rrange != nil {
|
||||||
|
switch ptr.Val(rrange.GetTypeEscaped()) {
|
||||||
|
case models.ENDDATE_RECURRENCERANGETYPE:
|
||||||
|
end := rrange.GetEndDate()
|
||||||
|
if end != nil {
|
||||||
|
// NOTE: We convert just a date into date+time in a
|
||||||
|
// different timezone which will cause it to not be just
|
||||||
|
// a date anymore.
|
||||||
|
endTime, err := getUTCTime(end.String(), ptr.Val(rrange.GetRecurrenceTimeZone()))
|
||||||
|
if err != nil {
|
||||||
|
return "", clues.Wrap(err, "parsing end time")
|
||||||
|
}
|
||||||
|
|
||||||
|
recurComponents = append(recurComponents, "UNTIL="+endTime.Format("20060102T150405Z"))
|
||||||
|
}
|
||||||
|
case models.NOEND_RECURRENCERANGETYPE:
|
||||||
|
// Nothing to do
|
||||||
|
case models.NUMBERED_RECURRENCERANGETYPE:
|
||||||
|
count := ptr.Val(rrange.GetNumberOfOccurrences())
|
||||||
|
if count > 0 {
|
||||||
|
recurComponents = append(recurComponents, "COUNT="+fmt.Sprint(count))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(recurComponents, ";"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromJSON(ctx context.Context, body []byte) (string, error) {
|
||||||
|
data, err := api.BytesToEventable(body)
|
||||||
|
if err != nil {
|
||||||
|
return "", clues.Wrap(err, "converting to eventable")
|
||||||
|
}
|
||||||
|
|
||||||
|
cal := ics.NewCalendar()
|
||||||
|
cal.SetProductId("-//Alcion//Corso") // Does this have to be customizable?
|
||||||
|
|
||||||
|
id := data.GetId() // XXX: iCalUId?
|
||||||
|
event := cal.AddEvent(ptr.Val(id))
|
||||||
|
|
||||||
|
created := data.GetCreatedDateTime()
|
||||||
|
if created != nil {
|
||||||
|
event.SetCreatedTime(ptr.Val(created))
|
||||||
|
}
|
||||||
|
|
||||||
|
modified := data.GetLastModifiedDateTime()
|
||||||
|
if modified != nil {
|
||||||
|
event.SetModifiedAt(ptr.Val(modified))
|
||||||
|
}
|
||||||
|
|
||||||
|
allDay := ptr.Val(data.GetIsAllDay())
|
||||||
|
|
||||||
|
startString := data.GetStart().GetDateTime()
|
||||||
|
timeZone := data.GetStart().GetTimeZone()
|
||||||
|
|
||||||
|
if startString != nil {
|
||||||
|
start, err := getUTCTime(ptr.Val(startString), ptr.Val(timeZone))
|
||||||
|
if err != nil {
|
||||||
|
return "", clues.Wrap(err, "parsing start time")
|
||||||
|
}
|
||||||
|
|
||||||
|
if allDay {
|
||||||
|
event.SetStartAt(start, ics.WithValue(string(ics.ValueDataTypeDate)))
|
||||||
|
} else {
|
||||||
|
event.SetStartAt(start)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endString := data.GetEnd().GetDateTime()
|
||||||
|
timeZone = data.GetEnd().GetTimeZone()
|
||||||
|
|
||||||
|
if endString != nil {
|
||||||
|
end, err := getUTCTime(ptr.Val(endString), ptr.Val(timeZone))
|
||||||
|
if err != nil {
|
||||||
|
return "", clues.Wrap(err, "parsing end time")
|
||||||
|
}
|
||||||
|
|
||||||
|
if allDay {
|
||||||
|
event.SetEndAt(end, ics.WithValue(string(ics.ValueDataTypeDate)))
|
||||||
|
} else {
|
||||||
|
event.SetEndAt(end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
recurrence := data.GetRecurrence()
|
||||||
|
if recurrence != nil {
|
||||||
|
pattern, err := getRecurrencePattern(ctx, recurrence)
|
||||||
|
if err != nil {
|
||||||
|
return "", clues.WrapWC(ctx, err, "generating RRULE")
|
||||||
|
}
|
||||||
|
|
||||||
|
event.AddRrule(pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelled := data.GetIsCancelled()
|
||||||
|
if cancelled != nil {
|
||||||
|
event.SetStatus(ics.ObjectStatusCancelled)
|
||||||
|
}
|
||||||
|
|
||||||
|
draft := data.GetIsDraft()
|
||||||
|
if draft != nil {
|
||||||
|
event.SetStatus(ics.ObjectStatusDraft)
|
||||||
|
}
|
||||||
|
|
||||||
|
summary := data.GetSubject()
|
||||||
|
if summary != nil {
|
||||||
|
event.SetSummary(ptr.Val(summary))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Emojies currently don't seem to be read properly by Outlook
|
||||||
|
bodyPreview := ptr.Val(data.GetBodyPreview())
|
||||||
|
|
||||||
|
if data.GetBody() != nil {
|
||||||
|
description := ptr.Val(data.GetBody().GetContent())
|
||||||
|
contentType := data.GetBody().GetContentType().String()
|
||||||
|
|
||||||
|
if len(description) > 0 && contentType == "text" {
|
||||||
|
event.SetDescription(description)
|
||||||
|
} else {
|
||||||
|
// https://stackoverflow.com/a/859475
|
||||||
|
event.SetDescription(bodyPreview)
|
||||||
|
|
||||||
|
if contentType == "html" {
|
||||||
|
desc := strings.ReplaceAll(description, "\r\n", "")
|
||||||
|
desc = strings.ReplaceAll(desc, "\n", "")
|
||||||
|
event.AddProperty("X-ALT-DESC", desc, ics.WithFmtType("text/html"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showAs := ptr.Val(data.GetShowAs()).String()
|
||||||
|
if len(showAs) > 0 && showAs != "unknown" {
|
||||||
|
var status ics.FreeBusyTimeType
|
||||||
|
|
||||||
|
switch showAs {
|
||||||
|
case "free":
|
||||||
|
status = ics.FreeBusyTimeTypeFree
|
||||||
|
case "tentative":
|
||||||
|
status = ics.FreeBusyTimeTypeBusyTentative
|
||||||
|
case "busy":
|
||||||
|
status = ics.FreeBusyTimeTypeBusy
|
||||||
|
case "oof", "workingElsewhere": // this is just best effort conversion
|
||||||
|
status = ics.FreeBusyTimeTypeBusyUnavailable
|
||||||
|
}
|
||||||
|
|
||||||
|
event.AddProperty(ics.ComponentPropertyFreebusy, string(status))
|
||||||
|
}
|
||||||
|
|
||||||
|
categories := data.GetCategories()
|
||||||
|
for _, category := range categories {
|
||||||
|
event.AddProperty(ics.ComponentPropertyCategories, category)
|
||||||
|
}
|
||||||
|
|
||||||
|
// According to the RFC, this property may be used in a calendar
|
||||||
|
// component to convey a location where a more dynamic rendition of
|
||||||
|
// the calendar information associated with the calendar component
|
||||||
|
// can be found.
|
||||||
|
url := ptr.Val(data.GetWebLink())
|
||||||
|
if len(url) > 0 {
|
||||||
|
event.SetURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
organizer := data.GetOrganizer()
|
||||||
|
if organizer != nil {
|
||||||
|
name := ptr.Val(organizer.GetEmailAddress().GetName())
|
||||||
|
addr := ptr.Val(organizer.GetEmailAddress().GetAddress())
|
||||||
|
|
||||||
|
// TODO: What to do if we only have a name?
|
||||||
|
if len(name) > 0 && len(addr) > 0 {
|
||||||
|
event.SetOrganizer(addr, ics.WithCN(name))
|
||||||
|
} else if len(addr) > 0 {
|
||||||
|
event.SetOrganizer(addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attendees := data.GetAttendees()
|
||||||
|
for _, attendee := range attendees {
|
||||||
|
props := []ics.PropertyParameter{}
|
||||||
|
|
||||||
|
atype := attendee.GetTypeEscaped()
|
||||||
|
if atype != nil {
|
||||||
|
var role ics.ParticipationRole
|
||||||
|
|
||||||
|
switch atype.String() {
|
||||||
|
case "required":
|
||||||
|
role = ics.ParticipationRoleReqParticipant
|
||||||
|
case "optional":
|
||||||
|
role = ics.ParticipationRoleOptParticipant
|
||||||
|
case "resource":
|
||||||
|
role = ics.ParticipationRoleNonParticipant
|
||||||
|
}
|
||||||
|
|
||||||
|
props = append(props, keyValues(string(ics.ParameterRole), string(role)))
|
||||||
|
}
|
||||||
|
|
||||||
|
name := ptr.Val(attendee.GetEmailAddress().GetName())
|
||||||
|
if len(name) > 0 {
|
||||||
|
props = append(props, ics.WithCN(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time when a resp change occurred is not recorded
|
||||||
|
if attendee.GetStatus() != nil {
|
||||||
|
resp := ptr.Val(attendee.GetStatus().GetResponse()).String()
|
||||||
|
if len(resp) > 0 && resp != "none" {
|
||||||
|
var pstat ics.ParticipationStatus
|
||||||
|
|
||||||
|
switch resp {
|
||||||
|
case "accepted", "organizer":
|
||||||
|
pstat = ics.ParticipationStatusAccepted
|
||||||
|
case "declined":
|
||||||
|
pstat = ics.ParticipationStatusDeclined
|
||||||
|
case "tentativelyAccepted":
|
||||||
|
pstat = ics.ParticipationStatusTentative
|
||||||
|
case "notResponded":
|
||||||
|
pstat = ics.ParticipationStatusNeedsAction
|
||||||
|
}
|
||||||
|
|
||||||
|
props = append(props, keyValues(string(ics.ParameterParticipationStatus), string(pstat)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := ptr.Val(attendee.GetEmailAddress().GetAddress())
|
||||||
|
event.AddAttendee(addr, props...)
|
||||||
|
}
|
||||||
|
|
||||||
|
location := getLocationString(data.GetLocation())
|
||||||
|
if len(location) > 0 {
|
||||||
|
event.SetLocation(location)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Handle different attachment type (file, item and reference)
|
||||||
|
attachments := data.GetAttachments()
|
||||||
|
for _, attachment := range attachments {
|
||||||
|
props := []ics.PropertyParameter{}
|
||||||
|
contentType := ptr.Val(attachment.GetContentType())
|
||||||
|
name := ptr.Val(attachment.GetName())
|
||||||
|
|
||||||
|
if len(name) > 0 {
|
||||||
|
// FILENAME does not seem to be parsed by Outlook
|
||||||
|
props = append(props,
|
||||||
|
&ics.KeyValues{
|
||||||
|
Key: "FILENAME",
|
||||||
|
Value: []string{name},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
cb, err := attachment.GetBackingStore().Get("contentBytes")
|
||||||
|
if err != nil {
|
||||||
|
return "", clues.Wrap(err, "getting attachment content")
|
||||||
|
}
|
||||||
|
|
||||||
|
content, ok := cb.([]uint8)
|
||||||
|
if !ok {
|
||||||
|
return "", clues.NewWC(ctx, "getting attachment content string")
|
||||||
|
}
|
||||||
|
|
||||||
|
props = append(props, ics.WithEncoding("base64"), ics.WithValue("BINARY"))
|
||||||
|
if len(contentType) > 0 {
|
||||||
|
props = append(props, ics.WithFmtType(contentType))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Inline attachments don't show up in Outlook
|
||||||
|
inline := ptr.Val(attachment.GetIsInline())
|
||||||
|
if inline {
|
||||||
|
cidv, err := attachment.GetBackingStore().Get("contentId")
|
||||||
|
if err != nil {
|
||||||
|
return "", clues.Wrap(err, "getting attachment content id")
|
||||||
|
}
|
||||||
|
|
||||||
|
cid, err := str.AnyToString(cidv)
|
||||||
|
if err != nil {
|
||||||
|
return "", clues.Wrap(err, "getting attachment content id string")
|
||||||
|
}
|
||||||
|
|
||||||
|
props = append(props, keyValues("CID", cid))
|
||||||
|
}
|
||||||
|
|
||||||
|
event.AddAttachment(base64.StdEncoding.EncodeToString(content), props...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cal.Serialize(), nil
|
||||||
|
}
|
||||||
1081
src/internal/converters/ics/ics_test.go
Normal file
1081
src/internal/converters/ics/ics_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/alcionai/clues"
|
"github.com/alcionai/clues"
|
||||||
|
|
||||||
"github.com/alcionai/corso/src/internal/converters/eml"
|
"github.com/alcionai/corso/src/internal/converters/eml"
|
||||||
|
"github.com/alcionai/corso/src/internal/converters/ics"
|
||||||
"github.com/alcionai/corso/src/internal/converters/vcf"
|
"github.com/alcionai/corso/src/internal/converters/vcf"
|
||||||
"github.com/alcionai/corso/src/internal/data"
|
"github.com/alcionai/corso/src/internal/data"
|
||||||
"github.com/alcionai/corso/src/pkg/control"
|
"github.com/alcionai/corso/src/pkg/control"
|
||||||
@ -48,12 +49,16 @@ func streamItems(
|
|||||||
|
|
||||||
for _, rc := range drc {
|
for _, rc := range drc {
|
||||||
ictx := clues.Add(ctx, "path_short_ref", rc.FullPath().ShortRef())
|
ictx := clues.Add(ctx, "path_short_ref", rc.FullPath().ShortRef())
|
||||||
|
|
||||||
ext := ".eml"
|
|
||||||
category := rc.FullPath().Category()
|
category := rc.FullPath().Category()
|
||||||
|
ext := ""
|
||||||
|
|
||||||
if category == path.ContactsCategory {
|
switch category {
|
||||||
|
case path.EmailCategory:
|
||||||
|
ext = ".eml"
|
||||||
|
case path.ContactsCategory:
|
||||||
ext = ".vcf"
|
ext = ".vcf"
|
||||||
|
case path.EventsCategory:
|
||||||
|
ext = ".ics"
|
||||||
}
|
}
|
||||||
|
|
||||||
for item := range rc.Items(ictx, errs) {
|
for item := range rc.Items(ictx, errs) {
|
||||||
@ -111,6 +116,20 @@ func streamItems(
|
|||||||
Error: err,
|
Error: err,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
case path.EventsCategory:
|
||||||
|
outData, err = ics.FromJSON(ctx, content)
|
||||||
|
if err != nil {
|
||||||
|
err = clues.Wrap(err, "converting to ics")
|
||||||
|
|
||||||
|
logger.CtxErr(ctx, err).Info("processing collection item")
|
||||||
|
|
||||||
|
ch <- export.Item{
|
||||||
|
ID: id,
|
||||||
|
Error: err,
|
||||||
|
}
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -69,6 +69,7 @@ var (
|
|||||||
SafeForTesting,
|
SafeForTesting,
|
||||||
HumanReadable,
|
HumanReadable,
|
||||||
HumanReadableDriveItem,
|
HumanReadableDriveItem,
|
||||||
|
M365DateTimeTimeZone,
|
||||||
Legacy,
|
Legacy,
|
||||||
TabularOutput,
|
TabularOutput,
|
||||||
ClippedHuman,
|
ClippedHuman,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user