Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 19 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ asdf plugin add calendarsync https://github.com/FeryET/asdf-calendarsync.git
## finally
asdf install calendarsync
```

Note: The `asdf` plugin is not managed by inovex, but is provided by a CalendarSync user. inovex assumes no responsibility for proper provisioning.

## First Time Execution
Expand All @@ -54,7 +55,7 @@ Then, start the app using `CALENDARSYNC_ENCRYPTION_KEY=<YourSecretPassword> ./ca

The app will create a file in the execution folder called `auth-storage.yaml`. In this file the OAuth2 Credentials will be saved encrypted by your `$CALENDARSYNC_ENCRYPTION_KEY`.

----
---

# Configuration

Expand Down Expand Up @@ -146,19 +147,21 @@ transformerOrder = []string{
"KeepTitle",
"PrefixTitle",
"ReplaceTitle",
"SetVisibility",
}

| **Name** | **Description** | **Configuration** |
|-------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------|
| `KeepAttendees` | Synchronizes the list of attendees. If `UseEmailAsDisplayName` is set to `true`, the email is used in the attendee list. Do not use when the Outlook Adapter is used as a sink as there is no way to suppress mail invitations. | `config.UseEmailAsDisplayName`, default `false` |
| `KeepLocation` | Synchronizes the location of the event. | – |
| `KeepReminders` | Synchronizes event reminders. | – |
| `KeepDescription` | Synchronizes the description of the event. | – |
| `KeepMeetingLink` | Adds the meeting link of the original meeting to the description of the event. | – |
| `AddOriginalLink` | Adds the link to the original calendar event to the description of the event. | – |
| `KeepTitle` | Synchronizes the event's title. Without this transformer, the title is set to `CalendarSync Event` | – |
| `PrefixTitle` | Adds the configured prefix to the title. | `config.Prefix`, default `""` |
| `ReplaceTitle` | Replaces the title with the configured string. Does not make sense to be used with `KeepTitle` or `PrefixTitle` | `config.NewTitle`, default `"CalendarSync Event"` |
| **Name** | **Description** | **Configuration** |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
| `KeepAttendees` | Synchronizes the list of attendees. If `UseEmailAsDisplayName` is set to `true`, the email is used in the attendee list. Do not use when the Outlook Adapter is used as a sink as there is no way to suppress mail invitations. | `config.UseEmailAsDisplayName`, default `false` |
| `KeepLocation` | Synchronizes the location of the event. | – |
| `KeepReminders` | Synchronizes event reminders. | – |
| `KeepDescription` | Synchronizes the description of the event. | – |
| `KeepMeetingLink` | Adds the meeting link of the original meeting to the description of the event. | – |
| `AddOriginalLink` | Adds the link to the original calendar event to the description of the event. | – |
| `KeepTitle` | Synchronizes the event's title. Without this transformer, the title is set to `CalendarSync Event` | – |
| `PrefixTitle` | Adds the configured prefix to the title. | `config.Prefix`, default `""` |
| `ReplaceTitle` | Replaces the title with the configured string. Does not make sense to be used with `KeepTitle` or `PrefixTitle` | `config.NewTitle`, default `"CalendarSync Event"` |
| `SetVisibility` | Sets the visibility of synced events. Supported values: `default`, `public`, `private`, `confidential`. Maps to iCalendar `CLASS` property. Supported by Google Calendar and Outlook adapters (Outlook maps `public` to `normal`). | `config.Visibility`, default `"default"` |

Example configuration:

Expand All @@ -176,6 +179,9 @@ transformations:
- name: KeepAttendees
config:
UseEmailAsDisplayName: true
- name: SetVisibility
config:
Visibility: "private"
```

## Filters
Expand Down Expand Up @@ -232,7 +238,7 @@ Corporation

# Relevant RFCs and Links

[RFC 5545](https://datatracker.ietf.org/doc/html/rfc5545) Internet Calendaring
[RFC 5545](https://datatracker.ietf.org/doc/html/rfc5545) Internet Calendaring
and Scheduling Core Object Specification (iCalendar) is used in the Google
calendar API to denote recurrence patterns. CalDav [RFC
4791](https://datatracker.ietf.org/doc/html/rfc4791) uses the dateformat
Expand Down
4 changes: 4 additions & 0 deletions example.sync.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ transformations:
- name: KeepAttendees
config:
UseEmailAsDisplayName: true
# Set event visibility: "default", "public", "private", or "confidential"
# - name: SetVisibility
# config:
# Visibility: "private"

# Filters remove events from being synced due to different criteria
filters:
Expand Down
14 changes: 14 additions & 0 deletions internal/adapter/google/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@ func (g *GCalClient) CreateEvent(ctx context.Context, event models.Event) error
}
}

// Set visibility, default to "default" if empty
visibility := event.Visibility
if visibility == "" {
visibility = "default"
}

call, err := retry(ctx, func() (*calendar.Event, error) {
g.RateLimiter.Take()
return g.Client.Events.Insert(g.CalendarId, &calendar.Event{
Expand All @@ -123,6 +129,7 @@ func (g *GCalClient) CreateEvent(ctx context.Context, event models.Event) error
ExtendedProperties: extProperties,
Attendees: calendarAttendees,
Reminders: &calendarReminders,
Visibility: visibility,
}).Context(ctx).SendUpdates("none").Do()
})
if err != nil {
Expand Down Expand Up @@ -165,6 +172,12 @@ func (g *GCalClient) UpdateEvent(ctx context.Context, event models.Event) error
}
}

// Set visibility, default to "default" if empty
visibility := event.Visibility
if visibility == "" {
visibility = "default"
}

_, err := retry(ctx, func() (*calendar.Event, error) {
g.RateLimiter.Take()
return g.Client.Events.Update(g.CalendarId, event.ID, &calendar.Event{
Expand All @@ -176,6 +189,7 @@ func (g *GCalClient) UpdateEvent(ctx context.Context, event models.Event) error
ExtendedProperties: extProperties,
Attendees: calendarAttendees,
Reminders: calendarReminders,
Visibility: visibility,
}).Context(ctx).SendUpdates("none").Do()
})
if isNotFound(err) {
Expand Down
7 changes: 7 additions & 0 deletions internal/adapter/google/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ func calendarEventToEvent(e *calendar.Event, adapterSourceID string) models.Even
}
}

// Set visibility, default to "default" if not specified
visibility := e.Visibility
if visibility == "" {
visibility = "default"
}

return models.Event{
ICalUID: e.ICalUID,
ID: e.Id,
Expand All @@ -52,6 +58,7 @@ func calendarEventToEvent(e *calendar.Event, adapterSourceID string) models.Even
Reminders: reminders,
MeetingLink: e.HangoutLink,
Accepted: hasEventAccepted,
Visibility: visibility,
}
}

Expand Down
35 changes: 35 additions & 0 deletions internal/adapter/outlook_http/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,36 @@ const (
ExtensionName = "inovex.calendarsync.meta"
)

// visibilityToSensitivity maps CalendarSync visibility values to Outlook sensitivity values
// Visibility: "default", "public", "private", "confidential" (iCalendar CLASS)
// Sensitivity: "normal", "personal", "private", "confidential" (Outlook/Graph API)
func visibilityToSensitivity(visibility string) string {
switch visibility {
case "public", "default", "":
return "normal"
case "private":
return "private"
case "confidential":
return "confidential"
default:
return "normal"
}
}

// sensitivityToVisibility maps Outlook sensitivity values to CalendarSync visibility values
func sensitivityToVisibility(sensitivity string) string {
switch sensitivity {
case "normal", "personal", "":
return "default"
case "private":
return "private"
case "confidential":
return "confidential"
default:
return "default"
}
}

// OutlookClient implements the OutlookCalendarClient interface
type OutlookClient struct {
Client *http.Client
Expand Down Expand Up @@ -249,6 +279,10 @@ func (o OutlookClient) eventToOutlookEvent(e models.Event) (oe Event) {
// we currently use the first reminder in the list, this may result in data loss
outlookEvent.ReminderMinutesBeforeStart = int(e.StartTime.Sub(e.Reminders[0].Trigger.PointInTime).Minutes())
}

// Map visibility to Outlook sensitivity
outlookEvent.Sensitivity = visibilityToSensitivity(e.Visibility)

return outlookEvent
}

Expand Down Expand Up @@ -303,6 +337,7 @@ func (o OutlookClient) outlookEventToEvent(oe Event, adapterSourceID string) (e
Reminders: reminders,
MeetingLink: oe.OnlineMeetingUrl,
Accepted: hasEventAccepted,
Visibility: sensitivityToVisibility(oe.Sensitivity),
}

if oe.IsAllDay {
Expand Down
1 change: 1 addition & 0 deletions internal/adapter/outlook_http/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type Event struct {
IsAllDay bool `json:"isAllDay"`
OnlineMeetingUrl string `json:"onlineMeetingUrl"`
ResponseStatus ResponseStatus `json:"responseStatus,omitempty"`
Sensitivity string `json:"sensitivity,omitempty"`
}

type Extensions struct {
Expand Down
7 changes: 7 additions & 0 deletions internal/models/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type Event struct {
Reminders Reminders
MeetingLink string
Accepted bool
Visibility string // Event visibility: "default", "public", "private", "confidential"
}

type Reminders []Reminder
Expand Down Expand Up @@ -113,6 +114,7 @@ func (e *Event) Overwrite(source Event) Event {
e.Location = source.Location
e.Reminders = source.Reminders
e.MeetingLink = source.MeetingLink
e.Visibility = source.Visibility

return *e
}
Expand Down Expand Up @@ -209,6 +211,11 @@ func IsSameEvent(a, b Event) bool {
}
}

if a.Visibility != b.Visibility {
log.Debugf("Visibility of Source Event %s at %s changed from %s to %s", a.Title, a.StartTime, b.Visibility, a.Visibility)
return false
}

return true
}

Expand Down
2 changes: 2 additions & 0 deletions internal/sync/transformer.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ var (
"KeepLocation": &transformation.KeepLocation{},
"KeepAttendees": &transformation.KeepAttendees{UseEmailAsDisplayName: false},
"KeepReminders": &transformation.KeepReminders{},
"SetVisibility": &transformation.SetVisibility{Visibility: "default"},
}

// this is the order of the transformers in which they get evaluated
Expand All @@ -52,6 +53,7 @@ var (
"KeepTitle",
"PrefixTitle",
"ReplaceTitle",
"SetVisibility",
}
)

Expand Down
33 changes: 33 additions & 0 deletions internal/transformation/setVisibility.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package transformation

import (
"github.com/inovex/CalendarSync/internal/models"
)

// SetVisibility allows to set the visibility of an event.
// Supported values: "default", "public", "private", "confidential"
type SetVisibility struct {
Visibility string
}

func (t *SetVisibility) Name() string {
return "SetVisibility"
}

func (t *SetVisibility) Transform(_ models.Event, sink models.Event) (models.Event, error) {
// Validate visibility value
validVisibilities := map[string]bool{
"default": true,
"public": true,
"private": true,
"confidential": true,
}

if t.Visibility != "" && validVisibilities[t.Visibility] {
sink.Visibility = t.Visibility
}
return sink, nil
}



78 changes: 78 additions & 0 deletions internal/transformation/setVisibility_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package transformation

import (
"testing"

"github.com/inovex/CalendarSync/internal/models"
"github.com/stretchr/testify/assert"
)

// verify transformer SetVisibility
func TestSetVisibility_Transform(t *testing.T) {
tt := []struct {
name string
visibility string
expectedVisibility string
}{
{
name: "set visibility to private",
visibility: "private",
expectedVisibility: "private",
},
{
name: "set visibility to public",
visibility: "public",
expectedVisibility: "public",
},
{
name: "set visibility to confidential",
visibility: "confidential",
expectedVisibility: "confidential",
},
{
name: "set visibility to default",
visibility: "default",
expectedVisibility: "default",
},
{
name: "invalid visibility is ignored",
visibility: "invalid",
expectedVisibility: "",
},
{
name: "empty visibility is ignored",
visibility: "",
expectedVisibility: "",
},
}

t.Parallel()
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
expectedTitle := "Test Event"

source := models.Event{
Title: expectedTitle,
Visibility: "default",
}
sink := models.Event{
Title: expectedTitle,
Visibility: "",
}

transformer := SetVisibility{Visibility: tc.visibility}
event, err := transformer.Transform(source, sink)

assert.Nil(t, err)

expectedEvent := models.Event{
Title: expectedTitle,
Visibility: tc.expectedVisibility,
}
assert.Equal(t, expectedEvent, event)
})
}
}