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