"""CAL-ADDRESS values from :rfc:`5545`."""
from typing import Any, ClassVar
from xml.etree.ElementTree import Element, SubElement
from icalendar.compatibility import Self
from icalendar.error import JCalParsingError
from icalendar.parser import Parameters
from icalendar.parser.xcal.base import XCalParser
from icalendar.parser.xcal.string import _to_valid_xml_string
from icalendar.parser.xcal.wrapper import from_xcal_wrapper
from icalendar.parser_tools import DEFAULT_ENCODING, to_unicode
[docs]
class vCalAddress(str):
r"""Calendar User Address
Value Name:
CAL-ADDRESS
Purpose:
This value type is used to identify properties that contain a
calendar user address.
Description:
The value is a URI as defined by [RFC3986] or any other
IANA-registered form for a URI. When used to address an Internet
email transport address for a calendar user, the value MUST be a
mailto URI, as defined by [RFC2368].
Example:
``mailto:`` is in front of the address.
.. code-block:: ics
mailto:jane_doe@example.com
Parsing:
.. code-block:: pycon
>>> from icalendar import vCalAddress
>>> cal_address = vCalAddress.from_ical('mailto:jane_doe@example.com')
>>> cal_address
vCalAddress('mailto:jane_doe@example.com')
Encoding:
.. code-block:: pycon
>>> from icalendar import vCalAddress, Event
>>> event = Event()
>>> jane = vCalAddress("mailto:jane_doe@example.com")
>>> jane.name = "Jane"
>>> event["organizer"] = jane
>>> print(event.to_ical().decode().replace('\\r\\n', '\\n').strip())
BEGIN:VEVENT
ORGANIZER;CN=Jane:mailto:jane_doe@example.com
END:VEVENT
"""
default_value: ClassVar[str] = "CAL-ADDRESS"
params: Parameters
__slots__ = ("params",)
def __new__(
cls,
value: str | bytes,
encoding: str = DEFAULT_ENCODING,
/,
params: dict[str, Any] | None = None,
) -> Self:
value = to_unicode(value, encoding=encoding)
if "\r" in value or "\n" in value:
raise ValueError(
f"A CAL-ADDRESS value may not contain CR or LF characters: {value!r}"
)
self = super().__new__(cls, value)
self.params = Parameters(params)
return self
def __repr__(self) -> str:
return f"vCalAddress('{self}')"
[docs]
def to_ical(self) -> bytes:
return self.encode(DEFAULT_ENCODING)
[docs]
@classmethod
def from_ical(cls, ical: str | bytes) -> Self:
return cls(ical)
def __eq__(self, other: object) -> bool:
"""Compare calendar addresses by email address.
Two addresses are equal if their email addresses match
case-insensitively. The ``mailto:`` prefix is optional on either
side, so ``mailto:user@example.com`` equals ``user@example.com``.
A plain :class:`str` is converted to :class:`vCalAddress` before
comparing, so ``"me@example.com" == vCalAddress("MAILTO:me@example.com")``
is ``True``. This is what lets the common pattern
``my_email in event.attendees`` work (see :pr:`1901`). If the
string cannot form a valid address (for example, it contains a newline),
comparison returns :data:`NotImplemented`, leaving the reflected
operation to decide.
Comparison against other types returns :data:`NotImplemented`.
Known limitation: hash consistency is guaranteed only between
two :class:`vCalAddress` instances. A plain :class:`str` hashes
by its literal value, so ``hash(vCalAddress(...))`` may differ
from ``hash(str(...))`` even when they compare equal. This is
accepted; see the discussion in :pr:`1901`.
See :issue:`1896`.
"""
if isinstance(other, vCalAddress):
return self.email.lower() == other.email.lower()
if isinstance(other, str):
try:
other = vCalAddress(other)
except ValueError:
# for example, the string contains CR or LF and cannot be an address
return NotImplemented
return self == other
return NotImplemented
def __ne__(self, other: object) -> bool:
"""Negation of :meth:`__eq__`.
This method is required because :class:`vCalAddress` extends
:class:`str`. Without it, ``!=`` would use :meth:`str.__ne__`
using plain string comparison and could disagree with
:meth:`__eq__`. For example, case-insensitively equal addresses would
be both ``==`` and ``!=`` at the same time.
"""
result = self.__eq__(other)
if result is NotImplemented:
return result
return not result
def __hash__(self) -> int:
"""Hash of the lowercased email address, consistent with :meth:`__eq__`."""
return hash(self.email.lower())
@property
def ical_value(self) -> str:
"""The ``mailto:`` part of the address."""
return str(self)
@property
def uri(self) -> str:
"""The calendar user address as a URI, usually the ``mailto:`` link.
This provides the same interface as :attr:`vUri.uri
<icalendar.prop.uri.vUri.uri>`.
Use :attr:`email` to get the address without ``mailto:``.
.. code-block:: pycon
>>> from icalendar import vCalAddress
>>> vCalAddress("mailto:jane_doe@example.com").uri
'mailto:jane_doe@example.com'
"""
return self.ical_value
@property
def email(self) -> str:
"""The email address without ``mailto:`` at the start."""
if self.lower().startswith("mailto:"):
return self[7:]
return str(self)
[docs]
def has_email(self, email: str) -> bool:
"""Check whether an email matches this calendar user.
The email can match either the calendar address or its ``EMAIL``
parameter.
Comparison ignores case and an optional ``mailto:`` prefix on either
address. This method does not validate email syntax.
Parameters:
email: Required. The address to compare, with or without ``mailto:``.
Returns:
bool: ``True`` if either address matches, otherwise ``False``.
Examples:
.. code-block:: pycon
>>> from icalendar import vCalAddress
>>> attendee = vCalAddress("mailto:calendar@example.com")
>>> attendee.EMAIL = "contact@example.com"
>>> attendee.has_email("CALENDAR@example.com")
True
>>> attendee.has_email("mailto:contact@example.com")
True
>>> attendee.has_email("other@example.com")
False
.. versionadded:: 0.0.0
"""
normalized = email.casefold().removeprefix("mailto:")
return any(
address is not None
and address.casefold().removeprefix("mailto:") == normalized
for address in (self.ical_value, self.EMAIL)
)
from icalendar.param import (
CN,
CUTYPE,
DELEGATED_FROM,
DELEGATED_TO,
DIR,
EMAIL,
LANGUAGE,
MEMBER,
PARTSTAT,
ROLE,
RSVP,
SENT_BY,
VALUE,
)
DELEGATED_FROM = DELEGATED_FROM
"""Specify the calendar users that delegated their participation.
This parameter can be specified on properties with a
CAL-ADDRESS value type. This parameter specifies those calendar
users that have delegated their participation in a group-scheduled
event or to-do to the calendar user specified by the property.
The individual calendar address parameter values MUST each be
specified in a quoted-string.
"""
DELEGATED_TO = DELEGATED_TO
"""Specify the calendar users to whom participation was delegated.
This parameter can be specified on properties with a
CAL-ADDRESS value type. This parameter specifies those calendar
users to whom participation in a group-scheduled event or to-do was
delegated by the calendar user specified by the property.
The individual calendar address parameter values MUST each be
specified in a quoted-string.
"""
MEMBER = MEMBER
"""Specify the group or list membership of a calendar user.
This parameter can be specified on properties with a
CAL-ADDRESS value type. The parameter identifies the groups or
list membership for the calendar user specified by the property.
The parameter value is either a single calendar address in a
quoted-string or a COMMA-separated list of calendar addresses,
each in a quoted-string. The individual calendar address
parameter values MUST each be specified in a quoted-string.
"""
name = CN
@staticmethod
def _get_email(email: str) -> str:
"""Extract email and add mailto: prefix if needed.
Handles case-insensitive mailto: prefix checking.
Parameters:
email: Email string that may or may not have mailto: prefix
Returns:
Email string with mailto: prefix
"""
if not email.lower().startswith("mailto:"):
return f"mailto:{email}"
return email
[docs]
@classmethod
def new(
cls,
email: str,
/,
cn: str | None = None,
cutype: str | None = None,
delegated_from: str | None = None,
delegated_to: str | None = None,
directory: str | None = None,
language: str | None = None,
partstat: str | None = None,
role: str | None = None,
rsvp: bool | None = None, # noqa: FBT001, RUF100
sent_by: str | None = None,
) -> Self:
"""Create a new vCalAddress with RFC 5545 parameters.
Creates a vCalAddress instance with automatic mailto: prefix handling
and support for all standard RFC 5545 parameters.
Parameters:
email: The email address (mailto: prefix added automatically if missing)
cn: Common Name parameter
cutype: Calendar user type (INDIVIDUAL, GROUP, RESOURCE, ROOM)
delegated_from: Email of the calendar user that delegated
delegated_to: Email of the calendar user that was delegated to
directory: Reference to directory information
language: Language for text values
partstat: Participation status (NEEDS-ACTION, ACCEPTED, DECLINED, etc.)
role: Role (REQ-PARTICIPANT, OPT-PARTICIPANT, NON-PARTICIPANT, CHAIR)
rsvp: Whether RSVP is requested
sent_by: Email of the calendar user acting on behalf of this user
Returns:
vCalAddress: A new calendar address with specified parameters
Raises:
TypeError: If email is not a string
Examples:
Basic usage:
>>> from icalendar.prop import vCalAddress
>>> addr = vCalAddress.new("test@test.com")
>>> str(addr)
'mailto:test@test.com'
With parameters:
>>> addr = vCalAddress.new("test@test.com", cn="Test User", role="CHAIR")
>>> addr.params["CN"]
'Test User'
>>> addr.params["ROLE"]
'CHAIR'
"""
if not isinstance(email, str):
raise TypeError(f"Email must be a string, not {type(email).__name__}")
# Handle mailto: prefix (case-insensitive)
email_with_prefix = cls._get_email(email)
# Create the address
addr = cls(email_with_prefix)
# Set parameters if provided
if cn is not None:
addr.params["CN"] = cn
if cutype is not None:
addr.params["CUTYPE"] = cutype
if delegated_from is not None:
addr.params["DELEGATED-FROM"] = cls._get_email(delegated_from)
if delegated_to is not None:
addr.params["DELEGATED-TO"] = cls._get_email(delegated_to)
if directory is not None:
addr.params["DIR"] = directory
if language is not None:
addr.params["LANGUAGE"] = language
if partstat is not None:
addr.params["PARTSTAT"] = partstat
if role is not None:
addr.params["ROLE"] = role
if rsvp is not None:
addr.params["RSVP"] = "TRUE" if rsvp else "FALSE"
if sent_by is not None:
addr.params["SENT-BY"] = cls._get_email(sent_by)
return addr
[docs]
def to_jcal(self, name: str) -> list:
"""Return this property in jCal format."""
return [name, self.params.to_jcal(), self.VALUE.lower(), self.ical_value]
[docs]
@classmethod
def examples(cls) -> list[Self]:
"""Examples of vCalAddress."""
return [cls.new("you@example.org", cn="You There")]
[docs]
@classmethod
def from_jcal(cls, jcal_property: list) -> Self:
"""Parse jCal from :rfc:`7265`.
Parameters:
jcal_property: The jCal property to parse.
Raises:
~icalendar.error.JCalParsingError: If the provided jCal is invalid.
"""
JCalParsingError.validate_property(jcal_property, cls)
JCalParsingError.validate_value_type(jcal_property[3], str, cls, 3)
return cls(
jcal_property[3],
params=Parameters.from_jcal_property(jcal_property),
)
[docs]
@classmethod
@from_xcal_wrapper
def from_xcal(cls, parser: XCalParser, params: Parameters) -> Self:
"""Parse xCal from :rfc:`6321`.
Parameters:
xml: The XML to parse or a parser.
Raises:
~error.XCalParsingError: If the provided xCal is invalid.
"""
element = parser.parse_tag(cls.default_value)
return cls(element.get_xsd_token(), params=params)
[docs]
def to_xcal(self, element: Element) -> None:
"""The xCal representation of this property according to :rfc:`6321`."""
self.params.to_xcal(element)
element = SubElement(element, "cal-address")
element.text = _to_valid_xml_string(self.ical_value)
__all__ = ["vCalAddress"]