1use crate::{CalendarObject, Error};
2use caldata::{
3 VcardParser,
4 component::{
5 CalendarInnerDataBuilder, ComponentMut, IcalAlarmBuilder, IcalCalendarObjectBuilder,
6 IcalEventBuilder, VcardContact,
7 },
8 generator::Emitter,
9 parser::{ContentLine, ParserOptions},
10 property::{
11 Calscale, IcalCALSCALEProperty, IcalDTENDProperty, IcalDTSTAMPProperty,
12 IcalDTSTARTProperty, IcalPRODIDProperty, IcalRRULEProperty, IcalSUMMARYProperty,
13 IcalUIDProperty, IcalVERSIONProperty, IcalVersion, VcardANNIVERSARYProperty,
14 VcardBDAYProperty, VcardFNProperty,
15 },
16 types::{CalDate, PartialDate, Tz},
17};
18use chrono::{NaiveDate, Utc};
19use hex::ToHex;
20use sha2::{Digest, Sha256};
21use std::str::FromStr;
22use std::{collections::BTreeMap, sync::OnceLock};
23
24#[derive(Debug, Clone)]
25pub struct AddressObject {
26 inner: VcardContact,
27 vcf: OnceLock<String>,
28}
29
30impl From<VcardContact> for AddressObject {
31 fn from(vcard: VcardContact) -> Self {
32 Self {
33 inner: vcard,
34 vcf: OnceLock::new(),
35 }
36 }
37}
38
39impl AddressObject {
40 pub fn from_vcf(vcf: String) -> Result<Self, Error> {
41 let parser = VcardParser::from_slice(vcf.as_bytes());
42 let vcard = parser.expect_one()?;
43 Ok(Self {
44 inner: vcard,
45 vcf: vcf.into(),
46 })
47 }
48
49 #[must_use]
50 pub fn get_etag(&self) -> String {
51 let mut hasher = Sha256::new();
52 hasher.update(self.get_vcf());
53 format!(
54 "\"{}\"",
55 hasher.finalize().as_slice().encode_hex::<String>()
56 )
57 }
58
59 #[must_use]
60 pub fn get_vcf(&self) -> &str {
61 self.vcf.get_or_init(|| self.inner.generate())
62 }
63
64 fn get_significant_date_object(
65 &self,
66 date: &PartialDate,
67 summary_prefix: &str,
68 suffix: &str,
69 ) -> Result<Option<CalendarObject>, Error> {
70 let Some(uid) = self.inner.get_uid() else {
71 return Ok(None);
72 };
73 let uid = format!("{uid}{suffix}");
74 let year = date.get_year();
75 let year_suffix = year.map(|year| format!(" {year}")).unwrap_or_default();
76 let Some(month) = date.get_month() else {
77 return Ok(None);
78 };
79 let Some(day) = date.get_day() else {
80 return Ok(None);
81 };
82 let Some(dtstart) = NaiveDate::from_ymd_opt(year.unwrap_or(1900), month, day) else {
83 return Ok(None);
84 };
85 let start_date = CalDate(dtstart, Tz::Local);
86 let Some(end_date) = start_date.succ_opt() else {
87 return Ok(None);
89 };
90 let Some(VcardFNProperty(fullname, _)) = self.inner.full_name.first() else {
91 return Ok(None);
92 };
93 let summary = format!("{summary_prefix} {fullname}{year_suffix}");
94
95 let event = IcalEventBuilder {
96 properties: vec![
97 IcalDTSTAMPProperty(Utc::now().into(), vec![].into()).into(),
98 IcalDTSTARTProperty(start_date.into(), vec![].into()).into(),
99 IcalDTENDProperty(end_date.into(), vec![].into()).into(),
100 IcalUIDProperty(uid, vec![].into()).into(),
101 IcalRRULEProperty(
102 caldata::rrule::RRule::from_str("FREQ=YEARLY").unwrap(),
103 vec![].into(),
104 )
105 .into(),
106 IcalSUMMARYProperty(summary.clone(), vec![].into()).into(),
107 ContentLine {
108 name: "TRANSP".to_owned(),
109 value: "TRANSPARENT".to_owned(),
110 ..Default::default()
111 },
112 ],
113 alarms: vec![IcalAlarmBuilder {
114 properties: vec![
115 ContentLine {
116 name: "TRIGGER".to_owned(),
117 value: "-PT0M".to_owned(),
118 params: vec![("VALUE".to_owned(), vec!["DURATION".to_owned()])].into(),
119 },
120 ContentLine {
121 name: "ACTION".to_owned(),
122 value: "DISPLAY".to_owned(),
123 ..Default::default()
124 },
125 ContentLine {
126 name: "DESCRIPTION".to_owned(),
127 value: summary,
128 ..Default::default()
129 },
130 ],
131 }],
132 };
133
134 Ok(Some(
135 IcalCalendarObjectBuilder {
136 properties: vec![
137 IcalVERSIONProperty(IcalVersion::Version2_0, vec![].into()).into(),
138 IcalCALSCALEProperty(Calscale::Gregorian, vec![].into()).into(),
139 IcalPRODIDProperty(
140 "-//github.com/lennart-k/rustical birthday calendar//EN".to_owned(),
141 vec![].into(),
142 )
143 .into(),
144 ],
145 inner: Some(CalendarInnerDataBuilder::Event(vec![event])),
146 vtimezones: BTreeMap::default(),
147 }
148 .build(&ParserOptions::default(), None)?
149 .into(),
150 ))
151 }
152
153 pub fn get_anniversary_object(&self) -> Result<Option<CalendarObject>, Error> {
154 let Some(VcardANNIVERSARYProperty(anniversary, _)) = &self.inner.anniversary else {
155 return Ok(None);
156 };
157 let Some(date) = &anniversary.date else {
158 return Ok(None);
159 };
160
161 self.get_significant_date_object(date, "💍", "-anniversary")
162 }
163
164 pub fn get_birthday_object(&self) -> Result<Option<CalendarObject>, Error> {
165 let Some(VcardBDAYProperty(bday, _)) = &self.inner.birthday else {
166 return Ok(None);
167 };
168 let Some(date) = &bday.date else {
169 return Ok(None);
170 };
171
172 self.get_significant_date_object(date, "🎂", "-birthday")
173 }
174
175 #[must_use]
176 pub const fn get_vcard(&self) -> &VcardContact {
177 &self.inner
178 }
179}