1"""CAL-ADDRESS values from :rfc:`5545`."""
2
3from typing import Any, ClassVar
4from xml.etree.ElementTree import Element, SubElement
5
6from icalendar.compatibility import Self
7from icalendar.error import JCalParsingError
8from icalendar.parser import Parameters
9from icalendar.parser.xcal.base import XCalParser
10from icalendar.parser.xcal.wrapper import from_xcal_wrapper
11from icalendar.parser_tools import DEFAULT_ENCODING, to_unicode
12
13
14class vCalAddress(str):
15 r"""Calendar User Address
16
17 Value Name:
18 CAL-ADDRESS
19
20 Purpose:
21 This value type is used to identify properties that contain a
22 calendar user address.
23
24 Description:
25 The value is a URI as defined by [RFC3986] or any other
26 IANA-registered form for a URI. When used to address an Internet
27 email transport address for a calendar user, the value MUST be a
28 mailto URI, as defined by [RFC2368].
29
30 Example:
31 ``mailto:`` is in front of the address.
32
33 .. code-block:: ics
34
35 mailto:jane_doe@example.com
36
37 Parsing:
38
39 .. code-block:: pycon
40
41 >>> from icalendar import vCalAddress
42 >>> cal_address = vCalAddress.from_ical('mailto:jane_doe@example.com')
43 >>> cal_address
44 vCalAddress('mailto:jane_doe@example.com')
45
46 Encoding:
47
48 .. code-block:: pycon
49
50 >>> from icalendar import vCalAddress, Event
51 >>> event = Event()
52 >>> jane = vCalAddress("mailto:jane_doe@example.com")
53 >>> jane.name = "Jane"
54 >>> event["organizer"] = jane
55 >>> print(event.to_ical().decode().replace('\\r\\n', '\\n').strip())
56 BEGIN:VEVENT
57 ORGANIZER;CN=Jane:mailto:jane_doe@example.com
58 END:VEVENT
59 """
60
61 default_value: ClassVar[str] = "CAL-ADDRESS"
62 params: Parameters
63 __slots__ = ("params",)
64
65 def __new__(
66 cls,
67 value: str | bytes,
68 encoding: str = DEFAULT_ENCODING,
69 /,
70 params: dict[str, Any] | None = None,
71 ) -> Self:
72 value = to_unicode(value, encoding=encoding)
73 if "\r" in value or "\n" in value:
74 raise ValueError(
75 f"A CAL-ADDRESS value may not contain CR or LF characters: {value!r}"
76 )
77 self = super().__new__(cls, value)
78 self.params = Parameters(params)
79 return self
80
81 def __repr__(self) -> str:
82 return f"vCalAddress('{self}')"
83
84 def to_ical(self) -> bytes:
85 return self.encode(DEFAULT_ENCODING)
86
87 @classmethod
88 def from_ical(cls, ical: str | bytes) -> Self:
89 return cls(ical)
90
91 @property
92 def ical_value(self) -> str:
93 """The ``mailto:`` part of the address."""
94 return str(self)
95
96 @property
97 def email(self) -> str:
98 """The email address without ``mailto:`` at the start."""
99 if self.lower().startswith("mailto:"):
100 return self[7:]
101 return str(self)
102
103 from icalendar.param import (
104 CN,
105 CUTYPE,
106 DELEGATED_FROM,
107 DELEGATED_TO,
108 DIR,
109 LANGUAGE,
110 MEMBER,
111 PARTSTAT,
112 ROLE,
113 RSVP,
114 SENT_BY,
115 VALUE,
116 )
117
118 DELEGATED_FROM = DELEGATED_FROM
119 """Specify the calendar users that delegated their participation.
120
121 This parameter can be specified on properties with a
122 CAL-ADDRESS value type. This parameter specifies those calendar
123 users that have delegated their participation in a group-scheduled
124 event or to-do to the calendar user specified by the property.
125 The individual calendar address parameter values MUST each be
126 specified in a quoted-string.
127 """
128
129 DELEGATED_TO = DELEGATED_TO
130 """Specify the calendar users to whom participation was delegated.
131
132 This parameter can be specified on properties with a
133 CAL-ADDRESS value type. This parameter specifies those calendar
134 users to whom participation in a group-scheduled event or to-do was
135 delegated by the calendar user specified by the property.
136 The individual calendar address parameter values MUST each be
137 specified in a quoted-string.
138 """
139
140 MEMBER = MEMBER
141 """Specify the group or list membership of a calendar user.
142
143 This parameter can be specified on properties with a
144 CAL-ADDRESS value type. The parameter identifies the groups or
145 list membership for the calendar user specified by the property.
146 The parameter value is either a single calendar address in a
147 quoted-string or a COMMA-separated list of calendar addresses,
148 each in a quoted-string. The individual calendar address
149 parameter values MUST each be specified in a quoted-string.
150 """
151
152 name = CN
153
154 @staticmethod
155 def _get_email(email: str) -> str:
156 """Extract email and add mailto: prefix if needed.
157
158 Handles case-insensitive mailto: prefix checking.
159
160 Parameters:
161 email: Email string that may or may not have mailto: prefix
162
163 Returns:
164 Email string with mailto: prefix
165 """
166 if not email.lower().startswith("mailto:"):
167 return f"mailto:{email}"
168 return email
169
170 @classmethod
171 def new(
172 cls,
173 email: str,
174 /,
175 cn: str | None = None,
176 cutype: str | None = None,
177 delegated_from: str | None = None,
178 delegated_to: str | None = None,
179 directory: str | None = None,
180 language: str | None = None,
181 partstat: str | None = None,
182 role: str | None = None,
183 rsvp: bool | None = None, # noqa: FBT001, RUF100
184 sent_by: str | None = None,
185 ) -> Self:
186 """Create a new vCalAddress with RFC 5545 parameters.
187
188 Creates a vCalAddress instance with automatic mailto: prefix handling
189 and support for all standard RFC 5545 parameters.
190
191 Parameters:
192 email: The email address (mailto: prefix added automatically if missing)
193 cn: Common Name parameter
194 cutype: Calendar user type (INDIVIDUAL, GROUP, RESOURCE, ROOM)
195 delegated_from: Email of the calendar user that delegated
196 delegated_to: Email of the calendar user that was delegated to
197 directory: Reference to directory information
198 language: Language for text values
199 partstat: Participation status (NEEDS-ACTION, ACCEPTED, DECLINED, etc.)
200 role: Role (REQ-PARTICIPANT, OPT-PARTICIPANT, NON-PARTICIPANT, CHAIR)
201 rsvp: Whether RSVP is requested
202 sent_by: Email of the calendar user acting on behalf of this user
203
204 Returns:
205 vCalAddress: A new calendar address with specified parameters
206
207 Raises:
208 TypeError: If email is not a string
209
210 Examples:
211 Basic usage:
212
213 >>> from icalendar.prop import vCalAddress
214 >>> addr = vCalAddress.new("test@test.com")
215 >>> str(addr)
216 'mailto:test@test.com'
217
218 With parameters:
219
220 >>> addr = vCalAddress.new("test@test.com", cn="Test User", role="CHAIR")
221 >>> addr.params["CN"]
222 'Test User'
223 >>> addr.params["ROLE"]
224 'CHAIR'
225 """
226 if not isinstance(email, str):
227 raise TypeError(f"Email must be a string, not {type(email).__name__}")
228
229 # Handle mailto: prefix (case-insensitive)
230 email_with_prefix = cls._get_email(email)
231
232 # Create the address
233 addr = cls(email_with_prefix)
234
235 # Set parameters if provided
236 if cn is not None:
237 addr.params["CN"] = cn
238 if cutype is not None:
239 addr.params["CUTYPE"] = cutype
240 if delegated_from is not None:
241 addr.params["DELEGATED-FROM"] = cls._get_email(delegated_from)
242 if delegated_to is not None:
243 addr.params["DELEGATED-TO"] = cls._get_email(delegated_to)
244 if directory is not None:
245 addr.params["DIR"] = directory
246 if language is not None:
247 addr.params["LANGUAGE"] = language
248 if partstat is not None:
249 addr.params["PARTSTAT"] = partstat
250 if role is not None:
251 addr.params["ROLE"] = role
252 if rsvp is not None:
253 addr.params["RSVP"] = "TRUE" if rsvp else "FALSE"
254 if sent_by is not None:
255 addr.params["SENT-BY"] = cls._get_email(sent_by)
256
257 return addr
258
259 def to_jcal(self, name: str) -> list:
260 """Return this property in jCal format."""
261 return [name, self.params.to_jcal(), self.VALUE.lower(), self.ical_value]
262
263 @classmethod
264 def examples(cls) -> list[Self]:
265 """Examples of vCalAddress."""
266 return [cls.new("you@example.org", cn="You There")]
267
268 @classmethod
269 def from_jcal(cls, jcal_property: list) -> Self:
270 """Parse jCal from :rfc:`7265`.
271
272 Parameters:
273 jcal_property: The jCal property to parse.
274
275 Raises:
276 ~error.JCalParsingError: If the provided jCal is invalid.
277 """
278 JCalParsingError.validate_property(jcal_property, cls)
279 JCalParsingError.validate_value_type(jcal_property[3], str, cls, 3)
280 return cls(
281 jcal_property[3],
282 params=Parameters.from_jcal_property(jcal_property),
283 )
284
285 @classmethod
286 @from_xcal_wrapper
287 def from_xcal(cls, parser: XCalParser, params: Parameters) -> Self:
288 """Parse xCal from :rfc:`6321`.
289
290 Parameters:
291 xml: The XML to parse or a parser.
292
293 Raises:
294 ~error.XCalParsingError: If the provided xCal is invalid.
295 """
296 element = parser.parse_tag(cls.default_value)
297 return cls(element.get_xsd_token(), params=params)
298
299 def to_xcal(self, element: Element) -> None:
300 """The xCal representation of this property according to :rfc:`6321`."""
301 self.params.to_xcal(element)
302 element = SubElement(element, "cal-address")
303 element.text = self.ical_value
304
305
306__all__ = ["vCalAddress"]