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