Compare commits

..

6 Commits

Author SHA1 Message Date
Lennart
8dfb47b28f version 0.4.2 2025-06-23 16:13:18 +02:00
Lennart
eb720ded99 ci: Only tag releases as latest container images 2025-06-23 16:12:36 +02:00
Lennart
89ef7b2ced Update vcard date tests 2025-06-23 16:09:22 +02:00
Lennart
6e0129130e Fix birthdays without year in birthday calendar
Fixes #79
2025-06-23 16:03:59 +02:00
Lennart
c646986c56 Version 0.4.1 2025-06-23 14:08:06 +02:00
Lennart
503cbe3699 fix: Add default frontend config 2025-06-23 14:07:38 +02:00
7 changed files with 120 additions and 85 deletions

View File

@@ -41,12 +41,10 @@ jobs:
# https://github.com/docker/metadata-action # https://github.com/docker/metadata-action
- name: Extract Docker metadata - name: Extract Docker metadata
id: meta id: meta
uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0 uses: docker/metadata-action@v5
with: with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# As long as we don't have releases everything on the main branch shall be tagged as latest
tags: | tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=ref,event=branch type=ref,event=branch
type=ref,event=pr type=ref,event=pr
type=semver,pattern={{version}} type=semver,pattern={{version}}

22
Cargo.lock generated
View File

@@ -2736,7 +2736,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical" name = "rustical"
version = "0.3.6" version = "0.4.2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argon2", "argon2",
@@ -2779,7 +2779,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_caldav" name = "rustical_caldav"
version = "0.3.6" version = "0.4.2"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -2814,7 +2814,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_carddav" name = "rustical_carddav"
version = "0.3.6" version = "0.4.2"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -2846,7 +2846,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_dav" name = "rustical_dav"
version = "0.3.6" version = "0.4.2"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -2871,7 +2871,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_dav_push" name = "rustical_dav_push"
version = "0.3.6" version = "0.4.2"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -2897,7 +2897,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_frontend" name = "rustical_frontend"
version = "0.3.6" version = "0.4.2"
dependencies = [ dependencies = [
"askama", "askama",
"askama_web", "askama_web",
@@ -2930,7 +2930,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_ical" name = "rustical_ical"
version = "0.3.6" version = "0.4.2"
dependencies = [ dependencies = [
"axum", "axum",
"chrono", "chrono",
@@ -2948,7 +2948,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_oidc" name = "rustical_oidc"
version = "0.3.6" version = "0.4.2"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -2963,7 +2963,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_store" name = "rustical_store"
version = "0.3.6" version = "0.4.2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -2997,7 +2997,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_store_sqlite" name = "rustical_store_sqlite"
version = "0.3.6" version = "0.4.2"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"chrono", "chrono",
@@ -3017,7 +3017,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_xml" name = "rustical_xml"
version = "0.3.6" version = "0.4.2"
dependencies = [ dependencies = [
"quick-xml", "quick-xml",
"thiserror 2.0.12", "thiserror 2.0.12",

View File

@@ -2,7 +2,7 @@
members = ["crates/*"] members = ["crates/*"]
[workspace.package] [workspace.package]
version = "0.3.6" version = "0.4.2"
edition = "2024" edition = "2024"
description = "A CalDAV server" description = "A CalDAV server"
repository = "https://github.com/lennart-k/rustical" repository = "https://github.com/lennart-k/rustical"

View File

@@ -12,3 +12,12 @@ pub struct FrontendConfig {
#[serde(default = "default_true")] #[serde(default = "default_true")]
pub allow_password_login: bool, pub allow_password_login: bool,
} }
impl Default for FrontendConfig {
fn default() -> Self {
Self {
enabled: true,
allow_password_login: true,
}
}
}

View File

@@ -62,14 +62,14 @@ impl AddressObject {
&self.vcf &self.vcf
} }
pub fn get_anniversary(&self) -> Option<CalDateTime> { pub fn get_anniversary(&self) -> Option<(CalDateTime, bool)> {
let prop = self.vcard.get_property("ANNIVERSARY")?; let prop = self.vcard.get_property("ANNIVERSARY")?.value.as_deref()?;
CalDateTime::parse_prop(prop, &HashMap::default()).ok() CalDateTime::parse_vcard(prop).ok()
} }
pub fn get_birthday(&self) -> Option<CalDateTime> { pub fn get_birthday(&self) -> Option<(CalDateTime, bool)> {
let prop = self.vcard.get_property("BDAY")?; let prop = self.vcard.get_property("BDAY")?.value.as_deref()?;
CalDateTime::parse_prop(prop, &HashMap::default()).ok() CalDateTime::parse_vcard(prop).ok()
} }
pub fn get_full_name(&self) -> Option<&str> { pub fn get_full_name(&self) -> Option<&str> {
@@ -78,25 +78,27 @@ impl AddressObject {
} }
pub fn get_anniversary_object(&self) -> Result<Option<CalendarObject>, Error> { pub fn get_anniversary_object(&self) -> Result<Option<CalendarObject>, Error> {
Ok(if let Some(anniversary) = self.get_anniversary() { Ok(
let fullname = if let Some(name) = self.get_full_name() { if let Some((anniversary, contains_year)) = self.get_anniversary() {
name let fullname = if let Some(name) = self.get_full_name() {
} else { name
return Ok(None); } else {
}; return Ok(None);
let anniversary = anniversary.date(); };
let year = anniversary.year(); let anniversary = anniversary.date();
let anniversary_start = anniversary.format(LOCAL_DATE); let year = contains_year.then_some(anniversary.year());
let anniversary_end = anniversary let anniversary_start = anniversary.format(LOCAL_DATE);
.succ_opt() let anniversary_end = anniversary
.unwrap_or(anniversary) .succ_opt()
.format(LOCAL_DATE); .unwrap_or(anniversary)
let uid = format!("{}-anniversary", self.get_id()); .format(LOCAL_DATE);
let uid = format!("{}-anniversary", self.get_id());
Some(CalendarObject::from_ics( let year_suffix = year.map(|year| format!(" ({year})")).unwrap_or_default();
uid.clone(), Some(CalendarObject::from_ics(
format!( uid.clone(),
r#"BEGIN:VCALENDAR format!(
r#"BEGIN:VCALENDAR
VERSION:2.0 VERSION:2.0
CALSCALE:GREGORIAN CALSCALE:GREGORIAN
PRODID:-//github.com/lennart-k/rustical birthday calendar//EN PRODID:-//github.com/lennart-k/rustical birthday calendar//EN
@@ -105,39 +107,42 @@ DTSTART;VALUE=DATE:{anniversary_start}
DTEND;VALUE=DATE:{anniversary_end} DTEND;VALUE=DATE:{anniversary_end}
UID:{uid} UID:{uid}
RRULE:FREQ=YEARLY RRULE:FREQ=YEARLY
SUMMARY:💍 {fullname} ({year}) SUMMARY:💍 {fullname}{year_suffix}
TRANSP:TRANSPARENT TRANSP:TRANSPARENT
BEGIN:VALARM BEGIN:VALARM
TRIGGER;VALUE=DURATION:-PT0M TRIGGER;VALUE=DURATION:-PT0M
ACTION:DISPLAY ACTION:DISPLAY
DESCRIPTION:💍 {fullname} ({year}) DESCRIPTION:💍 {fullname}{year_suffix}
END:VALARM END:VALARM
END:VEVENT END:VEVENT
END:VCALENDAR"#, END:VCALENDAR"#,
), ),
)?) )?)
} else { } else {
None None
}) },
)
} }
pub fn get_birthday_object(&self) -> Result<Option<CalendarObject>, Error> { pub fn get_birthday_object(&self) -> Result<Option<CalendarObject>, Error> {
Ok(if let Some(birthday) = self.get_birthday() { Ok(
let fullname = if let Some(name) = self.get_full_name() { if let Some((birthday, contains_year)) = self.get_birthday() {
name let fullname = if let Some(name) = self.get_full_name() {
} else { name
return Ok(None); } else {
}; return Ok(None);
let birthday = birthday.date(); };
let year = birthday.year(); let birthday = birthday.date();
let birthday_start = birthday.format(LOCAL_DATE); let year = contains_year.then_some(birthday.year());
let birthday_end = birthday.succ_opt().unwrap_or(birthday).format(LOCAL_DATE); let birthday_start = birthday.format(LOCAL_DATE);
let uid = format!("{}-birthday", self.get_id()); let birthday_end = birthday.succ_opt().unwrap_or(birthday).format(LOCAL_DATE);
let uid = format!("{}-birthday", self.get_id());
Some(CalendarObject::from_ics( let year_suffix = year.map(|year| format!(" ({year})")).unwrap_or_default();
uid.clone(), Some(CalendarObject::from_ics(
format!( uid.clone(),
r#"BEGIN:VCALENDAR format!(
r#"BEGIN:VCALENDAR
VERSION:2.0 VERSION:2.0
CALSCALE:GREGORIAN CALSCALE:GREGORIAN
PRODID:-//github.com/lennart-k/rustical birthday calendar//EN PRODID:-//github.com/lennart-k/rustical birthday calendar//EN
@@ -146,20 +151,21 @@ DTSTART;VALUE=DATE:{birthday_start}
DTEND;VALUE=DATE:{birthday_end} DTEND;VALUE=DATE:{birthday_end}
UID:{uid} UID:{uid}
RRULE:FREQ=YEARLY RRULE:FREQ=YEARLY
SUMMARY:🎂 {fullname} ({year}) SUMMARY:🎂 {fullname}{year_suffix}
TRANSP:TRANSPARENT TRANSP:TRANSPARENT
BEGIN:VALARM BEGIN:VALARM
TRIGGER;VALUE=DURATION:-PT0M TRIGGER;VALUE=DURATION:-PT0M
ACTION:DISPLAY ACTION:DISPLAY
DESCRIPTION:🎂 {fullname} ({year}) DESCRIPTION:🎂 {fullname}{year_suffix}
END:VALARM END:VALARM
END:VEVENT END:VEVENT
END:VCALENDAR"#, END:VCALENDAR"#,
), ),
)?) )?)
} else { } else {
None None
}) },
)
} }
/// Get significant dates associated with this address object /// Get significant dates associated with this address object

View File

@@ -254,6 +254,16 @@ impl CalDateTime {
if let Ok(date) = NaiveDate::parse_from_str(value, "%Y%m%d") { if let Ok(date) = NaiveDate::parse_from_str(value, "%Y%m%d") {
return Ok(CalDateTime::Date(date, timezone)); return Ok(CalDateTime::Date(date, timezone));
} }
Err(CalDateTimeError::InvalidDatetimeFormat(value.to_string()))
}
// Also returns whether the date contains a year
pub fn parse_vcard(value: &str) -> Result<(Self, bool), CalDateTimeError> {
if let Ok(datetime) = Self::parse(value, None) {
return Ok((datetime, true));
}
if let Some(captures) = RE_VCARD_DATE_MM_DD.captures(value) { if let Some(captures) = RE_VCARD_DATE_MM_DD.captures(value) {
// Because 1972 is a leap year // Because 1972 is a leap year
let year = 1972; let year = 1972;
@@ -261,13 +271,15 @@ impl CalDateTime {
let month = captures.name("m").unwrap().as_str().parse().ok().unwrap(); let month = captures.name("m").unwrap().as_str().parse().ok().unwrap();
let day = captures.name("d").unwrap().as_str().parse().ok().unwrap(); let day = captures.name("d").unwrap().as_str().parse().ok().unwrap();
return Ok(CalDateTime::Date( return Ok((
NaiveDate::from_ymd_opt(year, month, day) CalDateTime::Date(
.ok_or(CalDateTimeError::ParseError(value.to_string()))?, NaiveDate::from_ymd_opt(year, month, day)
timezone, .ok_or(CalDateTimeError::ParseError(value.to_string()))?,
CalTimezone::Local,
),
false,
)); ));
} }
Err(CalDateTimeError::InvalidDatetimeFormat(value.to_string())) Err(CalDateTimeError::InvalidDatetimeFormat(value.to_string()))
} }
@@ -407,24 +419,33 @@ mod tests {
#[test] #[test]
fn test_vcard_date() { fn test_vcard_date() {
assert_eq!( assert_eq!(
CalDateTime::parse("19850412", None).unwrap(), CalDateTime::parse_vcard("19850412").unwrap(),
CalDateTime::Date( (
NaiveDate::from_ymd_opt(1985, 4, 12).unwrap(), CalDateTime::Date(
crate::CalTimezone::Local NaiveDate::from_ymd_opt(1985, 4, 12).unwrap(),
crate::CalTimezone::Local
),
true
) )
); );
assert_eq!( assert_eq!(
CalDateTime::parse("1985-04-12", None).unwrap(), CalDateTime::parse_vcard("1985-04-12").unwrap(),
CalDateTime::Date( (
NaiveDate::from_ymd_opt(1985, 4, 12).unwrap(), CalDateTime::Date(
crate::CalTimezone::Local NaiveDate::from_ymd_opt(1985, 4, 12).unwrap(),
crate::CalTimezone::Local
),
true
) )
); );
assert_eq!( assert_eq!(
CalDateTime::parse("--0412", None).unwrap(), CalDateTime::parse_vcard("--0412").unwrap(),
CalDateTime::Date( (
NaiveDate::from_ymd_opt(1972, 4, 12).unwrap(), CalDateTime::Date(
crate::CalTimezone::Local NaiveDate::from_ymd_opt(1972, 4, 12).unwrap(),
crate::CalTimezone::Local
),
false
) )
); );
} }

View File

@@ -79,6 +79,7 @@ pub struct Config {
pub data_store: DataStoreConfig, pub data_store: DataStoreConfig,
#[serde(default)] #[serde(default)]
pub http: HttpConfig, pub http: HttpConfig,
#[serde(default)]
pub frontend: FrontendConfig, pub frontend: FrontendConfig,
#[serde(default)] #[serde(default)]
pub oidc: Option<OidcConfig>, pub oidc: Option<OidcConfig>,