Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/icalendar/prop/cal_address.py: 49%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

118 statements  

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 def __eq__(self, other: object) -> bool: 

92 """Compare calendar addresses by email address. 

93 

94 Two addresses are equal if their email addresses match 

95 case-insensitively. The ``mailto:`` prefix is optional on either 

96 side, so ``mailto:user@example.com`` equals ``user@example.com``. 

97 

98 A plain :class:`str` is converted to :class:`vCalAddress` before 

99 comparing, so ``"me@example.com" == vCalAddress("MAILTO:me@example.com")`` 

100 is ``True``. This is what lets the common pattern 

101 ``my_email in event.attendees`` work (see :pr:`1901`). If the 

102 string cannot form a valid address (for example, it contains a newline), 

103 comparison returns :data:`NotImplemented`, leaving the reflected 

104 operation to decide. 

105 

106 Comparison against other types returns :data:`NotImplemented`. 

107 

108 Known limitation: hash consistency is guaranteed only between 

109 two :class:`vCalAddress` instances. A plain :class:`str` hashes 

110 by its literal value, so ``hash(vCalAddress(...))`` may differ 

111 from ``hash(str(...))`` even when they compare equal. This is 

112 accepted; see the discussion in :pr:`1901`. 

113 

114 See :issue:`1896`. 

115 """ 

116 if isinstance(other, vCalAddress): 

117 return self.email.lower() == other.email.lower() 

118 if isinstance(other, str): 

119 try: 

120 other = vCalAddress(other) 

121 except ValueError: 

122 # for example, the string contains CR or LF and cannot be an address 

123 return NotImplemented 

124 return self == other 

125 return NotImplemented 

126 

127 def __ne__(self, other: object) -> bool: 

128 """Negation of :meth:`__eq__`. 

129 

130 This method is required because :class:`vCalAddress` extends 

131 :class:`str`. Without it, ``!=`` would use :meth:`str.__ne__` 

132 using plain string comparison and could disagree with 

133 :meth:`__eq__`. For example, case-insensitively equal addresses would 

134 be both ``==`` and ``!=`` at the same time. 

135 """ 

136 result = self.__eq__(other) 

137 if result is NotImplemented: 

138 return result 

139 return not result 

140 

141 def __hash__(self) -> int: 

142 """Hash of the lowercased email address, consistent with :meth:`__eq__`.""" 

143 return hash(self.email.lower()) 

144 

145 @property 

146 def ical_value(self) -> str: 

147 """The ``mailto:`` part of the address.""" 

148 return str(self) 

149 

150 @property 

151 def uri(self) -> str: 

152 """The calendar user address as a URI, usually the ``mailto:`` link. 

153 

154 This provides the same interface as :attr:`vUri.uri 

155 <icalendar.prop.uri.vUri.uri>`. 

156 Use :attr:`email` to get the address without ``mailto:``. 

157 

158 .. code-block:: pycon 

159 

160 >>> from icalendar import vCalAddress 

161 >>> vCalAddress("mailto:jane_doe@example.com").uri 

162 'mailto:jane_doe@example.com' 

163 """ 

164 return self.ical_value 

165 

166 @property 

167 def email(self) -> str: 

168 """The email address without ``mailto:`` at the start.""" 

169 if self.lower().startswith("mailto:"): 

170 return self[7:] 

171 return str(self) 

172 

173 def has_email(self, email: str) -> bool: 

174 """Check whether an email matches this calendar user. 

175 

176 The email can match either the calendar address or its ``EMAIL`` 

177 parameter. 

178 Comparison ignores case and an optional ``mailto:`` prefix on either 

179 address. This method does not validate email syntax. 

180 

181 Parameters: 

182 email: Required. The address to compare, with or without ``mailto:``. 

183 

184 Returns: 

185 bool: ``True`` if either address matches, otherwise ``False``. 

186 

187 Examples: 

188 .. code-block:: pycon 

189 

190 >>> from icalendar import vCalAddress 

191 >>> attendee = vCalAddress("mailto:calendar@example.com") 

192 >>> attendee.EMAIL = "contact@example.com" 

193 >>> attendee.has_email("CALENDAR@example.com") 

194 True 

195 >>> attendee.has_email("mailto:contact@example.com") 

196 True 

197 >>> attendee.has_email("other@example.com") 

198 False 

199 

200 .. versionadded:: 0.0.0 

201 """ 

202 normalized = email.casefold().removeprefix("mailto:") 

203 return any( 

204 address is not None 

205 and address.casefold().removeprefix("mailto:") == normalized 

206 for address in (self.ical_value, self.EMAIL) 

207 ) 

208 

209 from icalendar.param import ( 

210 CN, 

211 CUTYPE, 

212 DELEGATED_FROM, 

213 DELEGATED_TO, 

214 DIR, 

215 EMAIL, 

216 LANGUAGE, 

217 MEMBER, 

218 PARTSTAT, 

219 ROLE, 

220 RSVP, 

221 SENT_BY, 

222 VALUE, 

223 ) 

224 

225 DELEGATED_FROM = DELEGATED_FROM 

226 """Specify the calendar users that delegated their participation. 

227 

228 This parameter can be specified on properties with a 

229 CAL-ADDRESS value type. This parameter specifies those calendar 

230 users that have delegated their participation in a group-scheduled 

231 event or to-do to the calendar user specified by the property. 

232 The individual calendar address parameter values MUST each be 

233 specified in a quoted-string. 

234 """ 

235 

236 DELEGATED_TO = DELEGATED_TO 

237 """Specify the calendar users to whom participation was delegated. 

238 

239 This parameter can be specified on properties with a 

240 CAL-ADDRESS value type. This parameter specifies those calendar 

241 users to whom participation in a group-scheduled event or to-do was 

242 delegated by the calendar user specified by the property. 

243 The individual calendar address parameter values MUST each be 

244 specified in a quoted-string. 

245 """ 

246 

247 MEMBER = MEMBER 

248 """Specify the group or list membership of a calendar user. 

249 

250 This parameter can be specified on properties with a 

251 CAL-ADDRESS value type. The parameter identifies the groups or 

252 list membership for the calendar user specified by the property. 

253 The parameter value is either a single calendar address in a 

254 quoted-string or a COMMA-separated list of calendar addresses, 

255 each in a quoted-string. The individual calendar address 

256 parameter values MUST each be specified in a quoted-string. 

257 """ 

258 

259 name = CN 

260 

261 @staticmethod 

262 def _get_email(email: str) -> str: 

263 """Extract email and add mailto: prefix if needed. 

264 

265 Handles case-insensitive mailto: prefix checking. 

266 

267 Parameters: 

268 email: Email string that may or may not have mailto: prefix 

269 

270 Returns: 

271 Email string with mailto: prefix 

272 """ 

273 if not email.lower().startswith("mailto:"): 

274 return f"mailto:{email}" 

275 return email 

276 

277 @classmethod 

278 def new( 

279 cls, 

280 email: str, 

281 /, 

282 cn: str | None = None, 

283 cutype: str | None = None, 

284 delegated_from: str | None = None, 

285 delegated_to: str | None = None, 

286 directory: str | None = None, 

287 language: str | None = None, 

288 partstat: str | None = None, 

289 role: str | None = None, 

290 rsvp: bool | None = None, # noqa: FBT001, RUF100 

291 sent_by: str | None = None, 

292 ) -> Self: 

293 """Create a new vCalAddress with RFC 5545 parameters. 

294 

295 Creates a vCalAddress instance with automatic mailto: prefix handling 

296 and support for all standard RFC 5545 parameters. 

297 

298 Parameters: 

299 email: The email address (mailto: prefix added automatically if missing) 

300 cn: Common Name parameter 

301 cutype: Calendar user type (INDIVIDUAL, GROUP, RESOURCE, ROOM) 

302 delegated_from: Email of the calendar user that delegated 

303 delegated_to: Email of the calendar user that was delegated to 

304 directory: Reference to directory information 

305 language: Language for text values 

306 partstat: Participation status (NEEDS-ACTION, ACCEPTED, DECLINED, etc.) 

307 role: Role (REQ-PARTICIPANT, OPT-PARTICIPANT, NON-PARTICIPANT, CHAIR) 

308 rsvp: Whether RSVP is requested 

309 sent_by: Email of the calendar user acting on behalf of this user 

310 

311 Returns: 

312 vCalAddress: A new calendar address with specified parameters 

313 

314 Raises: 

315 TypeError: If email is not a string 

316 

317 Examples: 

318 Basic usage: 

319 

320 >>> from icalendar.prop import vCalAddress 

321 >>> addr = vCalAddress.new("test@test.com") 

322 >>> str(addr) 

323 'mailto:test@test.com' 

324 

325 With parameters: 

326 

327 >>> addr = vCalAddress.new("test@test.com", cn="Test User", role="CHAIR") 

328 >>> addr.params["CN"] 

329 'Test User' 

330 >>> addr.params["ROLE"] 

331 'CHAIR' 

332 """ 

333 if not isinstance(email, str): 

334 raise TypeError(f"Email must be a string, not {type(email).__name__}") 

335 

336 # Handle mailto: prefix (case-insensitive) 

337 email_with_prefix = cls._get_email(email) 

338 

339 # Create the address 

340 addr = cls(email_with_prefix) 

341 

342 # Set parameters if provided 

343 if cn is not None: 

344 addr.params["CN"] = cn 

345 if cutype is not None: 

346 addr.params["CUTYPE"] = cutype 

347 if delegated_from is not None: 

348 addr.params["DELEGATED-FROM"] = cls._get_email(delegated_from) 

349 if delegated_to is not None: 

350 addr.params["DELEGATED-TO"] = cls._get_email(delegated_to) 

351 if directory is not None: 

352 addr.params["DIR"] = directory 

353 if language is not None: 

354 addr.params["LANGUAGE"] = language 

355 if partstat is not None: 

356 addr.params["PARTSTAT"] = partstat 

357 if role is not None: 

358 addr.params["ROLE"] = role 

359 if rsvp is not None: 

360 addr.params["RSVP"] = "TRUE" if rsvp else "FALSE" 

361 if sent_by is not None: 

362 addr.params["SENT-BY"] = cls._get_email(sent_by) 

363 

364 return addr 

365 

366 def to_jcal(self, name: str) -> list: 

367 """Return this property in jCal format.""" 

368 return [name, self.params.to_jcal(), self.VALUE.lower(), self.ical_value] 

369 

370 @classmethod 

371 def examples(cls) -> list[Self]: 

372 """Examples of vCalAddress.""" 

373 return [cls.new("you@example.org", cn="You There")] 

374 

375 @classmethod 

376 def from_jcal(cls, jcal_property: list) -> Self: 

377 """Parse jCal from :rfc:`7265`. 

378 

379 Parameters: 

380 jcal_property: The jCal property to parse. 

381 

382 Raises: 

383 ~icalendar.error.JCalParsingError: If the provided jCal is invalid. 

384 """ 

385 JCalParsingError.validate_property(jcal_property, cls) 

386 JCalParsingError.validate_value_type(jcal_property[3], str, cls, 3) 

387 return cls( 

388 jcal_property[3], 

389 params=Parameters.from_jcal_property(jcal_property), 

390 ) 

391 

392 @classmethod 

393 @from_xcal_wrapper 

394 def from_xcal(cls, parser: XCalParser, params: Parameters) -> Self: 

395 """Parse xCal from :rfc:`6321`. 

396 

397 Parameters: 

398 xml: The XML to parse or a parser. 

399 

400 Raises: 

401 ~error.XCalParsingError: If the provided xCal is invalid. 

402 """ 

403 element = parser.parse_tag(cls.default_value) 

404 return cls(element.get_xsd_token(), params=params) 

405 

406 def to_xcal(self, element: Element) -> None: 

407 """The xCal representation of this property according to :rfc:`6321`.""" 

408 self.params.to_xcal(element) 

409 element = SubElement(element, "cal-address") 

410 element.text = self.ical_value 

411 

412 

413__all__ = ["vCalAddress"]