You don't need a 33K LOC enterprise library to read a calendar file.
Most apps just need to create a meeting invite, parse an .ics export, or read a contacts file. You shouldn't need to understand RFC 5545 to do that.
ezcal is iCalendar + vCard in one crate. Builder pattern in, valid .ics / .vcf out. Parse, modify, write back — unknown properties preserved.
cargo add ezcal
Got a meeting? Turn it into a .ics file any calendar app can open.
use ezcal::ical::{Calendar, Event};
let cal = Calendar::new()
.event(
Event::new()
.summary("Team Standup")
.location("Room 42")
.starts("2026-03-15T09:00:00")
.ends("2026-03-15T09:30:00")
)
.build();
std::fs::write("meeting.ics", cal.to_string())?;
// → Opens in Google Calendar, Apple Calendar, OutlookGot a .ics file from Google Calendar? Read it in two lines.
use ezcal::ical::Calendar;
let calendar = Calendar::parse(&std::fs::read_to_string("export.ics")?)?;
for event in calendar.events() {
println!("{}: {}",
event.get_starts().unwrap(),
event.get_summary().unwrap_or("(untitled)")
);
}Create .vcf files that import into any contacts app.
use ezcal::vcard::Contact;
let card = Contact::new()
.full_name("Jane Doe")
.email("jane@example.com")
.phone("+1-555-0123")
.organization("Acme Corp")
.build();
std::fs::write("jane.vcf", card.to_string())?;Parse .vcf exports — even files with hundreds of contacts.
use ezcal::vcard::Contact;
let contacts = Contact::parse_all(&std::fs::read_to_string("contacts.vcf")?)?;
for c in &contacts {
println!("{}: {}", c.get_full_name().unwrap(), c.get_email().unwrap_or("(no email)"));
}| You Want To | Use This | Result |
|---|---|---|
| Create a calendar event | Calendar::new().event(Event::new()...) |
Valid .ics file |
| Parse an .ics file | Calendar::parse(&text) |
Typed Calendar struct |
| Create a contact card | Contact::new().full_name(...) |
Valid .vcf file |
| Parse a .vcf file | Contact::parse_all(&text) |
Vec of Contact structs |
| Add recurring events | .rrule(RecurrenceRule::parse("FREQ=WEEKLY")?) |
RRULE in your event |
| Add reminders | .alarm(Alarm::display("-PT15M", "Soon!")) |
VALARM in your event |
| Work with timezones | .starts_dt(DateTimeValue::DateTimeTz{...}) |
TZID parameter set |
| Convert to chrono | dt.to_chrono_utc() |
DateTime<Utc> |
Problem: "Users book appointments, I need to send them a calendar invite."
use ezcal::ical::{Calendar, Event, Alarm};
fn create_booking(title: &str, start: &str, end: &str, location: &str) -> String {
Calendar::new()
.event(
Event::new()
.summary(title)
.location(location)
.starts(start)
.ends(end)
.status("CONFIRMED")
.alarm(Alarm::display("-PT15M", "Appointment starting soon"))
)
.build()
.to_string()
}
// Attach to email or serve as HTTP response
let ics = create_booking("Haircut", "2026-03-15T10:00:00", "2026-03-15T10:30:00", "Main St Salon");Problem: "Export from one calendar, import to another."
use ezcal::ical::Calendar;
let cal = Calendar::parse(&std::fs::read_to_string("google-export.ics")?)?;
println!("Migrating {} events, {} todos", cal.events().len(), cal.todos().len());
for event in cal.events() {
let summary = event.get_summary().unwrap_or("(untitled)");
let start = event.get_starts().map(|d| d.to_string()).unwrap_or_default();
// Insert into your new system...
println!(" {} @ {}", summary, start);
}Problem: "Let users export their contacts as a .vcf file."
use ezcal::vcard::{Contact, Address, StructuredName};
fn export_contacts(users: &[User]) -> String {
let mut output = String::new();
for user in users {
let card = Contact::new()
.full_name(&user.name)
.email(&user.email)
.phone(&user.phone)
.organization(&user.company)
.build();
output.push_str(&card.to_string());
}
output
}Problem: "Set up a weekly standup that repeats for a year."
use ezcal::ical::{Calendar, Event, RecurrenceRule};
let cal = Calendar::new()
.event(
Event::new()
.summary("Weekly Standup")
.starts("2026-01-05T09:00:00")
.ends("2026-01-05T09:30:00")
.rrule(RecurrenceRule::parse("FREQ=WEEKLY;BYDAY=MO;COUNT=52").unwrap())
.add_category("MEETING")
)
.build();Problem: "Export tasks to a format other apps can read."
use ezcal::ical::{Calendar, Todo};
let cal = Calendar::new()
.todo(Todo::new().summary("Ship v1.0").due_date("2026-04-01").priority(1).status("NEEDS-ACTION"))
.todo(Todo::new().summary("Write docs").due_date("2026-03-15").priority(3).status("IN-PROCESS"))
.todo(Todo::new().summary("Fix bug #42").status("COMPLETED").completed_date("2026-03-01"))
.build();
std::fs::write("tasks.ics", cal.to_string())?;| Type | What It Does | Key Methods |
|---|---|---|
Calendar |
.ics container | new(), parse(), event(), todo(), build() |
Event |
VEVENT component | summary(), starts(), ends(), location(), rrule(), alarm() |
Todo |
VTODO component | summary(), due_date(), status(), priority(), percent_complete() |
Alarm |
VALARM component | display(trigger, desc), audio(trigger) |
RecurrenceRule |
RRULE | parse(str), new(Frequency) |
| Type | What It Does | Key Methods |
|---|---|---|
Contact |
.vcf container | new(), parse(), parse_all(), full_name(), email(), phone() |
StructuredName |
N property | new(family, given), with_prefix(), with_suffix() |
Address |
ADR property | new(), street(), city(), region(), postal_code(), country() |
| Type | What It Does | Key Methods |
|---|---|---|
DateTimeValue |
Date/time values | parse(), to_chrono_utc(), from_chrono_utc(), to_property() |
| Library | iCal | vCard | Read + Write | Status |
|---|---|---|---|---|
| ezcal | Yes | Yes | Yes | Active |
ical |
Yes | No | Read only | Archived (Aug 2024) |
icalendar |
Yes | No | Write-focused | Active |
calcard |
Yes | Yes | Yes | 33K LOC, complex API |
Output tested against:
- Google Calendar (.ics import)
- Apple Calendar (.ics import)
- Outlook (.ics import)
- Google Contacts (.vcf import)
- Apple Contacts (.vcf import)
MIT