Skip to content

magicdude4eva/calendar-sync

Repository files navigation

📅 calendar-sync

GitHub stars Build Python GitHub forks GitHub issues GitHub last commit License

calendar-sync is a flexible utility to sync one or more ICS feeds (iCalendar) into a CalDAV-compatible calendar — ideal for mailbox.org, Nextcloud, Synology, and more.

It supports features such as:

  • ✅ Deterministic UID generation for clean deduplication
  • 📅 Emoji mapping for more readable calendar events
  • 🔁 Automatic expansion of RRULE:FREQ=YEARLY events
  • 🔁 Full support for recurring events (e.g., yearly holidays) and custom extra events (Mother's Day, Advent Sundays, etc.)
  • 🧼 Cleanup mode with multi-prefix support (--cleanup PREFIX1,PREFIX2)
  • 📍 Location-based filtering (e.g., for regional holidays in Austria)
  • 🐳 Docker deployment for simple automation
  • 💡 Dry-run mode to preview changes without writing
  • 🕓 Timezone-aware handling for accurate scheduling

This is perfect for importing:

  • 🗑️ Municipal waste collection schedules (e.g., Müll App)
  • 🇦🇹 Austrian public holidays
  • 🏎️ Formula 1 calendar with free practice, qualifying, and GP events

Unlike subscription-based ICS calendars, this tool writes events directly into your calendar, giving you full control over notifications, offline visibility, and data retention.

Use it on your Synology NAS, a server, or as a cron-triggered Docker container — and never miss a bin collection or Grand Prix again.

calendar-sync.mp4


paypal


paypal 🍺 Please support me: Although all my software is free, it is always appreciated if you can support my efforts on Github with a contribution via Paypal - this allows me to write cool projects like this in my personal time and hopefully help you or your business.


✨ Features

  • 🔁 Sync multiple ICS feeds to any CalDAV calendar
  • 🧠 Deterministic UID generation & deduplication
  • 🔁 Automatic expansion of YEARLY recurring events
  • 📍 Location-based filtering for region-specific holidays
  • 🧹 Optional cleanup of old imported events
  • 📅 Supports emoji mapping for event names
  • 🛑 Dry run mode to test before writing
  • 🐳 Docker support for simple deployment

🚀 Usage

Manual

# Default config file (config.json)
python src/calendar_sync.py --import
python src/calendar_sync.py --import --dry-run
python src/calendar_sync.py --cleanup # cleans global prefix
python src/calendar_sync.py --cleanup MUELL-,F1- # cleans multiple prefixes

# Custom config file
python src/calendar_sync.py --import --config /path/to/another_config.json
python src/calendar_sync.py --import --dry-run --config /path/to/another_config.json
python src/calendar_sync.py --cleanup --config /path/to/another_config.json

With Docker Compose

First, build the container:

docker-compose build

Then run the sync:

# Default config file
docker-compose run --rm calendar-sync --import
docker-compose run --rm calendar-sync --import --dry-run
docker-compose run --rm calendar-sync --cleanup

# Custom config file (mount the config file into the container)
docker-compose run --rm -v /path/to/another_config.json:/app/config.json calendar-sync --import
docker-compose run --rm -v /path/to/another_config.json:/app/config.json calendar-sync --import --dry-run
docker-compose run --rm -v /path/to/another_config.json:/app/config.json calendar-sync --cleanup

🧰 Manual Installation

git clone https://github.com/magicdude4eva/calendar-sync.git
cd calendar-sync
python3.14 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

⚙️ Configuration

You can use multiple config files to manage different calendars. By default, the script uses config.json. To use a different config file, pass it via the --config argument:

python src/calendar_sync.py --import --config /path/to/another_config.json

Example config:

{
  "caldav_url": "https://dav-sso.mailbox.org/caldav/...",
  "username": "your@email.com",
  "password": "your-app-password",
  "timezone": "Europe/Vienna",
  "uid_prefix": "ICS-",
  "future_event_limit_days": 365,
  "ics_feeds": [
    {
      "url": "https://example.com/my.ics",
      "uid_prefix": "EXAMPLE-",
      "emoji_mapping": {
        "Papier": "♻️",
        "default": "📦"
      },
      "default_reminder": "1d"
    }
  ]
}

🛠️ How It Works

  • Fetches events from each configured ICS feed
  • Normalizes dates and checks if the UID exists
  • Skips, adds, or replaces events as needed
  • Uses emoji mappings to prefix event names
  • All-day events are handled properly (no time zone shift)
  • Recurring RRULE:FREQ=YEARLY events are expanded into individual years
  • Events can be filtered by LOCATION using import_locations

🗺️ For import_locations, configure it per feed. For example:

{
  "url": "https://www.feiertage-oesterreich.at/kalender-download/ics/feiertage-oesterreich.ics",
  "import_locations": "K,St,V",
  "emoji_mapping": {
    "§": "🇦🇹",
    "default": "🗓️"
  }
}

To discover valid locations, run the sync once and check the logs. Example:

INFO: ⏭️ Skipping 'St. Florian' (2025-05-04) due to unmatched location: OÖ

📱 Emoji Mapping

Emoji mapping allows you to prefix event titles with emojis for better visual organization in your calendar. This is particularly useful when syncing multiple ICS feeds to distinguish between different event types at a glance.

How it works:

The emoji_mapping object in your feed configuration uses event title matching to determine which emoji to prepend to an event:

  1. Exact key matching: The script checks if any key in the emoji_mapping object matches part of the event's title
  2. Prepending: When a match is found, the corresponding emoji is prepended to the event title
  3. Fallback: If the event title doesn't match any configured keys, the "default" emoji is used
  4. Case-sensitive: Matching is case-sensitive

Key points:

  • The emoji_mapping object accepts any string as a key (words, phrases, special characters, etc.)
  • The "default" key is a special reserved field that serves as a fallback emoji when no other keys match
  • Values must be emojis or text that will be prepended to the event title
  • You can configure multiple mappings per feed

Examples:

{
  "url": "https://example.com/waste.ics",
  "emoji_mapping": {
    "Papier": "♻️",
    "Plastik": "🟡",
    "Glas": "🟢",
    "Restmüll": "",
    "default": "🗑️"
  }
}

With the above configuration, an event titled "Papier Collection" would become "♻️ Papier Collection", while an unknown event type would become "🗑️ Unknown Event".

Another example (Austrian holidays):

{
  "url": "https://www.feiertage-oesterreich.at/kalender-download/ics/feiertage-oesterreich.ics",
  "emoji_mapping": {
    "Ostern": "🐰",
    "Weihnachten": "🎄",
    "Neujahr": "🎆",
    "§": "🇦🇹",
    "default": "🗓️"
  }
}

🏷️ Categories Support

Categories allow you to organize and filter events in your calendar client. image

The categories configuration provides comprehensive control over how event categories are handled:

Category Configuration Options

The categories object in your feed configuration supports multiple operations:

Option Type Description Example
append Array Add categories to the end of existing ones ["Imported", "Synced"]
prepend Array Add categories to the beginning of existing ones ["Custom"]
remove Array Remove unwanted categories ["SPAM", "ADVERTISING"]
replace_if_empty Array Replace all categories if event has none ["Default"]
deduplicate Boolean Remove duplicate categories (default: true) true or false

How Categories Work

  1. Upstream categories are preserved from the ICS feed
  2. Custom categories are added based on your configuration
  3. Filtering removes unwanted categories
  4. Deduplication ensures each category appears only once

Category Processing Order

  1. Start with upstream categories from the ICS feed (if any)
  2. Apply replace_if_empty if event has no categories and this is configured
  3. Apply prepend categories (added to beginning)
  4. Apply append categories (added to end)
  5. Apply remove filtering
  6. Deduplicate if enabled

Examples

Example 1: Adding custom categories

{
  "url": "https://example.com/my.ics",
  "categories": {
    "append": ["Imported", "Synced"]
  }
}

Result: Events get their original categories + "Imported" and "Synced"

Example 2: Filtering unwanted categories

{
  "url": "https://example.com/third-party.ics",
  "categories": {
    "append": ["Imported"],
    "remove": ["SPAM", "ADVERTISING", "Third-Party"]
  }
}

Result: Events get original categories + "Imported", minus any unwanted ones

Example 3: Replacing empty categories

{
  "url": "https://example.com/empty-categories.ics",
  "categories": {
    "replace_if_empty": ["Default Category"],
    "append": ["Synced Events"]
  }
}

Result: Events with no categories get ["Default Category", "Synced Events"]

Example 4: Prepending categories

{
  "url": "https://example.com/priority.ics",
  "categories": {
    "prepend": ["High Priority", "Important"]
  }
}

Result: Events get ["High Priority", "Important"] + original categories

Example 5: Complete control

{
  "url": "https://example.com/clean.ics",
  "categories": {
    "prepend": ["Custom"],
    "append": ["Imported"],
    "remove": ["SPAM"],
    "deduplicate": true
  }
}

Result: Full control over category composition with deduplication

Use Cases

  • Identify synced events: Add "Imported" or "Synced" to all events
  • Filter spam: Remove unwanted categories like "SPAM" or "ADVERTISING"
  • Organize by type: Add categories like "Holidays", "Sports", "Work"
  • Clean up third-party feeds: Remove feed-specific spam categories
  • Standardize categories: Ensure consistent categorization across feeds

Client-Side Coloring

Many calendar clients (Thunderbird, Evolution, etc.) support category-based coloring:

  • Events with the same category share the same color
  • You can configure your client to assign specific colors to specific categories
  • This provides visual organization without modifying the event data

Note: Color assignment is client-specific and not part of the CalDAV standard. See your calendar client's documentation for details.

Example Thunderbird setup:

  1. Go to Calendar Properties
  2. Select "Categories" tab
  3. Assign colors to each category
  4. Events will automatically use those colors

Reference: Thunderbird Calendar Categories

🎨 Event Colors (RFC 7986)

You can assign colors to individual events using the calendar_color property in your feed configuration. This follows the RFC 7986 standard for iCalendar color properties. For supported colors in refer to CSS Color Names.

Color Configuration

Add calendar_color to any feed configuration:

{
  "url": "https://example.com/my.ics",
  "calendar_color": "#FF5733",  // Orange-red color
  "categories": {
    "append": ["Important"]
  }
}

Supported Color Formats

Format Example Description
Hex (3-digit) #RGB #F00 for red
Hex (6-digit) #RRGGBB #FF5733 for orange-red
Color name red, blue, green Standard color names

Color Examples

{
  "ics_feeds": [
    {
      "url": "https://app.muellapp.com/ical/74418",
      "calendar_color": "#4CAF50",  // Green for waste collection
      "categories": {
        "append": ["Waste Collection", "Municipal"]
      }
    },
    {
      "url": "https://feiertage-oesterreich.at/kalender-download/ics/feiertage-oesterreich.ics",
      "calendar_color": "#FF5733",  // Orange for holidays
      "categories": {
        "prepend": ["Austrian"]
      }
    },
    {
      "url": "https://better-f1-calendar.vercel.app/api/calendar.ics",
      "calendar_color": "#2196F3",  // Blue for F1 racing
      "categories": {
        "append": ["Sports", "Racing"]
      }
    }
  ]
}

How It Works

  1. The calendar_color property is added to each event from the feed
  2. The color is embedded in the iCalendar data (RFC 7986 compliant)
  3. Modern CalDAV clients that support RFC 7986 will display these colors
  4. The color assignment is per-event and can vary by feed

Client Support

  • Thunderbird: Supports RFC 7986 color property
  • Evolution: Supports RFC 7986 color property
  • Nextcloud Calendar: Supports RFC 7986 color property
  • mailbox.org: Supports RFC 7986 color property
  • ⚠️ Some mobile clients: May not support RFC 7986 yet

Note: Even if a client doesn't support RFC 7986, the color information is stored in the event and won't cause issues.

Fallback Behavior

If your CalDAV client doesn't support RFC 7986 color properties, the events will still be created normally. The color information will be embedded in the event data but may be ignored by the client.

Use Cases

  • Visual organization: Different feeds use different colors
  • Quick identification: Waste collection vs holidays vs sports events
  • Calendar segmentation: Each type of event has its own color
  • Client-side filtering: Use colors to identify event categories

Limitations

  • Color assignment is client-dependent
  • Not all CalDAV clients support RFC 7986 yet
  • Colors may not be visible in all calendar applications
  • The color property is additional metadata, not required for functionality

⏰ Default Reminder Format

The default_reminder field specifies when you should receive a notification for imported events. It uses a duration format with unit suffixes:

Format Meaning
15m 15 minutes before the event
1h 1 hour before the event
1d 1 day before the event
2d 2 days before the event
1w 1 week before the event
  • m = minutes
  • h = hours
  • d = days
  • w = weeks

Examples:

{
  "ics_feeds": [
    {
      "url": "https://example.com/my.ics",
      "default_reminder": "15m"   // Notify 15 minutes before
    },
    {
      "url": "https://example.com/holidays.ics",
      "default_reminder": "1d"    // Notify 1 day before
    },
    {
      "url": "https://example.com/formula1.ics",
      "default_reminder": "2h"    // Notify 2 hours before
    }
  ]
}

🗓️ Support for Yearly Recurring Events (RRULE:FREQ=YEARLY)

The script automatically expands ICS events with RRULE:FREQ=YEARLY rules into individual event instances for each year, up to the configured future limit (future_event_limit_days). This ensures recurring events like public holidays or anniversaries are correctly synced across multiple years.

Behavior:

  • Detects yearly recurring events by scanning raw RRULE data.
  • Expands the base event for each year (e.g. from 2025 to 2026).
  • Skips events in the past or beyond the future limit.
  • Deduplicates intelligently using UID hashing per year.

➕ Support for Custom Extra Events

In addition to ICS feeds, you can define your own custom events using the extra_events entry in config.json.

This allows you to add things like:

  • 🌷 Mother's Day (2nd Sunday of May)
  • 👨‍👧‍👦 Father's Day (2nd Sunday of June)
  • 🔥 Summer Solstice (21st June)
  • 🎃 Halloween (31st October)
  • 🕯️ Advent Sundays
  • 🧾 Tax Deadlines
  • ☀️ Daylight Saving Time changes

Supported Formats:

Format Description Example
N.Weekday.Month Nth weekday of a month 2.Sunday.5 → 2nd Sunday in May
-N.Weekday.Month Nth weekday from end of month -1.Sunday.3 → last Sunday in March
DD.MM.fixed Fixed date 31.10.fixed → 31st October

Sample:

    "extra_events": [
      "☀️ Sommerzeit beginnt:-1.Sunday.3",
      "🌷 Muttertag:2.Sunday.5",
      "👨‍👧‍👦 Vatertag:2.Sunday.6",
      "🔥 Sonnwendfeier:21.6.fixed",
      "🧾 Steuererklärung:30.6.fixed",
      "🌒 Sommerzeit endet:-1.Sunday.10",
      "🎃 Halloween:31.10.fixed",
      "🕯️ 1. Advent:-4.Sunday.12",
      "🕯️ 2. Advent:-3.Sunday.12",
      "🕯️ 3. Advent:-2.Sunday.12",
      "🕯️ 4. Advent:-1.Sunday.12",
      "👹 Krampusnacht:5.12.fixed",
      "🎅 Nikolaus:6.12.fixed"    
      ],

🧪 Dry Run

Add --dry-run to see what would happen without making changes:

docker-compose run --rm calendar-sync --import --dry-run

🗂️ Project Structure

calendar-sync/
├── src/
│   ├── calendar_sync.py      # Entry script
│   └── utils.py              # Core sync logic
├── config.json               # Configuration
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
└── README.md

📷 mailbox.org Setup Guide

1. 🔐 Create Application Password

Go to Settings → Security → Application Passwords
Select Calendar and Addressbook Client (CalDAV/CardDAV)
Create Application Password


2. 📅 Create a New Calendar

Go to the Calendar section → click + Add new calendar
Create a New Calendar


3. 🔗 Get the CalDAV URL

Right-click your new calendar → Properties → Copy the URL
Get the CalDAV URL

Paste it into config.json under "caldav_url"

📄 License

This project is licensed under the MIT License.


❤️ Contributing

PRs welcome! File issues or ideas via GitHub.

Donations are always welcome

🍻 Support my work
All my software is free and built in my personal time. If it helps you or your business, please consider a small donation via PayPal — it keeps the coffee ☕ and ideas flowing!

💸 Crypto Donations
You can also send crypto to one of the addresses below:

(BTC)   bc1qdgdkk7l98pje8ny9u4xavsvrea8dw6yu8jpnyf
(ETH)   0x5986f713A538D6bCaC0865564dCD45E2600A3469  
(POL)   0x5986f713A538D6bCaC0865564dCD45E2600A3469
(CRO)   0xb83c3Fe378F5224fAdD7a0f8a7dD33a6C96C422C (Cronos or Crypto.com Paystring magicdude$paystring.crypto.com)
(BNB)   0x5986f713A538D6bCaC0865564dCD45E2600A3469
(LTC)   ltc1qexst2exxksfyg7erfzlfrm23twkjgf7e5fn64t
(DOGE)  DMQsxc9XGF6526drBJDZeX7AjFDJsEz4mN
(SOL)   t4bYQCUuoCUrp7kJ4Mz314npcTuKoUSXj28UgdMrfTb

🧾 Recommended Platforms

  • 👉 Curve.com: Add your Crypto.com card to Apple Pay
  • 🔐 Crypto.com: Stake and get your free Crypto Visa card
  • 📈 Binance: Trade altcoins easily

About

Sync ICS feeds like holidays, waste pickup, and F1 calendars into your CalDAV calendar (e.g., mailbox.org). Supports emoji mapping, recurring events, location filters, deduplication, and Docker automation.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors