예제 #1
0
def validate_text_parameter(parameter):
    """
    text-param, as described on page 35
    <http://tools.ietf.org/html/rfc2426#section-4>

    @param parameter: Single parameter, as returned by get_vcard_property_param

    Examples:
    >>> validate_text_parameter(['VALUE', {'ptext'}])
    """
    param_name = parameter[0].upper()
    param_values = parameter[1]

    if param_name == 'VALUE':
        if param_values != {'ptext'}:
            raise VCardFormatError(
                '{0}: {1}'.format(MSG_INVALID_PARAM_VALUE, param_values), {})
        return
    elif param_name == 'LANGUAGE':
        if len(param_values) != 1:
            raise VCardFormatError(
                '{0}: {1}'.format(MSG_INVALID_PARAM_VALUE, param_values), {})
        for param_value in param_values:
            validate_language_tag(param_value)
    else:
        validate_x_name(param_name)
        if len(param_values) != 1:
            raise VCardFormatError(
                '{0}: {1}'.format(MSG_INVALID_PARAM_VALUE, param_values), {})
        validate_param_value(param_values[0])
예제 #2
0
def validate_time_zone(text):
    """
    Based on http://tools.ietf.org/html/rfc2425#section-5.8.4 and the fact
    that it specifies a subset of ISO 8601.

    @param text: String

    Examples:
    >>> validate_time_zone('Z')
    >>> validate_time_zone('+01:00')
    >>> validate_time_zone('-12:30')
    >>> validate_time_zone('+23:59')
    >>> validate_time_zone('-0001')
    >>> validate_time_zone('-00:30')
    >>> validate_time_zone('+00:30')
    >>> validate_time_zone('Z+01:00') # Can't combine Z and offset
    ... # doctest: +ELLIPSIS
    Traceback (most recent call last):
    VCardFormatError: Invalid time zone ...
    String: Z+01:00
    >>> validate_time_zone('+1:00') # Need preceding zero
    ... # doctest: +ELLIPSIS
    Traceback (most recent call last):
    VCardFormatError: Invalid time zone ...
    String: +1:00
    >>> validate_time_zone('0100') # Need + or -
    ... # doctest: +ELLIPSIS
    Traceback (most recent call last):
    VCardFormatError: Invalid time zone ...
    String: 0100
    >>> validate_time_zone('01') # Need colon and minutes
    ... # doctest: +ELLIPSIS
    Traceback (most recent call last):
    VCardFormatError: Invalid time zone ...
    String: 01
    >>> validate_time_zone('01:') # Need minutes
    ... # doctest: +ELLIPSIS
    Traceback (most recent call last):
    VCardFormatError: Invalid time zone ...
    String: 01:
    >>> validate_time_zone('01:1') # Need preceding zero
    ... # doctest: +ELLIPSIS
    Traceback (most recent call last):
    VCardFormatError: Invalid time zone ...
    """
    if not VALID_TZ.match(text):
        raise VCardFormatError(MSG_INVALID_TIME_ZONE, {'String': text})

    try:
        isodate.parse_tzinfo(text.replace('+', 'Z+').replace('-', 'Z-'))
    except (isodate.ISO8601Error, ValueError):
        raise VCardFormatError(MSG_INVALID_TIME_ZONE, {'String': text})
예제 #3
0
def validate_x_name(text):
    """
    @param text: Single parameter name

    Examples:
    >>> validate_x_name('X-abc')
    >>> validate_x_name('X-' + ID_CHARS)
    >>> validate_x_name('X-') # Have to have more characters
    Traceback (most recent call last):
    VCardFormatError: Invalid X-name (See RFC 2426 section 4 for x-name syntax)
    String: X-
    >>> validate_x_name('') # Have to start with X- #doctest: +ELLIPSIS
    Traceback (most recent call last):
    ...
    VCardFormatError: Invalid X-name (See RFC 2426 section 4 for x-name syntax)
    ...
    >>> validate_x_name('x-abc') # X must be upper case
    Traceback (most recent call last):
    VCardFormatError: Invalid X-name (See RFC 2426 section 4 for x-name syntax)
    String: x-abc
    >>> validate_x_name('foo') # Have to start with X-
    Traceback (most recent call last):
    VCardFormatError: Invalid X-name (See RFC 2426 section 4 for x-name syntax)
    String: foo
    """
    if VALID_X_NAME.match(text) is None:
        raise VCardFormatError(MSG_INVALID_X_NAME, {'String': text})
예제 #4
0
def validate_time(text):
    """
    Based on http://tools.ietf.org/html/rfc2425#section-5.8.4 and the fact
    that it specifies a subset of ISO 8601.

    @param text: String

    Examples:
    >>> validate_time('00:00:00')
    >>> validate_time('000000')
    >>> validate_time('01:02:03Z')
    >>> validate_time('01:02:03+01:30')
    >>> validate_time('01:02:60')
    Traceback (most recent call last):
    VCardFormatError: Invalid time (See RFC 2425 section 5.8.4 for time syntax)
    String: 01:02:60
    >>> validate_time('01:60:59')
    Traceback (most recent call last):
    VCardFormatError: Invalid time (See RFC 2425 section 5.8.4 for time syntax)
    String: 01:60:59
    >>> validate_time('24:00:00')
    Traceback (most recent call last):
    VCardFormatError: Invalid time (See RFC 2425 section 5.8.4 for time syntax)
    String: 24:00:00
    >>> validate_time('00:00:00Z+01') # doctest: +ELLIPSIS
    Traceback (most recent call last):
    VCardFormatError: Invalid time zone ...
    String: Z+01
    """
    time_timezone = VALID_TIME_TZ.match(text)
    if time_timezone is None:
        raise VCardFormatError(MSG_INVALID_TIME, {'String': text})

    time_str, timezone_str = time_timezone.groups()
    try:
        isodate.parse_time(time_str)
    except (isodate.ISO8601Error, ValueError):
        raise VCardFormatError(MSG_INVALID_TIME, {'String': text})

    if timezone_str == '':
        return

    validate_time_zone(timezone_str)
예제 #5
0
def validate_date(text):
    """
    Based on http://tools.ietf.org/html/rfc2425#section-5.8.4 and the fact
    that it specifies a subset of ISO 8601.

    @param text: String

    Examples:
    >>> validate_date('20000101')
    >>> validate_date('2000-01-01')
    >>> validate_date('2000:01:01') # Wrong separator
    Traceback (most recent call last):
    VCardFormatError: Invalid date (See RFC 2425 section 5.8.4 for date syntax)
    String: 2000:01:01
    >>> validate_date('2000101') # Too short
    Traceback (most recent call last):
    VCardFormatError: Invalid date (See RFC 2425 section 5.8.4 for date syntax)
    String: 2000101
    >>> validate_date('20080229')
    >>> validate_date('20100229') # Not a leap year
    Traceback (most recent call last):
    VCardFormatError: Invalid date (See RFC 2425 section 5.8.4 for date syntax)
    String: 20100229
    >>> validate_date('19000229') # Not a leap year (divisible by 100)
    Traceback (most recent call last):
    VCardFormatError: Invalid date (See RFC 2425 section 5.8.4 for date syntax)
    String: 19000229
    >>> validate_date('20000229') # Leap year (divisible by 400)
    >>> validate_date('aaaa-bb-cc')
    Traceback (most recent call last):
    VCardFormatError: Invalid date (See RFC 2425 section 5.8.4 for date syntax)
    String: aaaa-bb-cc
    """
    if VALID_DATE.match(text) is None:
        raise VCardFormatError(MSG_INVALID_DATE, {'String': text})

    try:
        isodate.parse_date(text)
    except (isodate.ISO8601Error, ValueError):
        raise VCardFormatError(MSG_INVALID_DATE, {'String': text})
예제 #6
0
def validate_ptext(text):
    """
    ptext, as described on page 28
    <http://tools.ietf.org/html/rfc2426#section-4>

    @param text: String

    Examples:
    >>> validate_ptext('')
    >>> validate_ptext(SAFE_CHARS)
    >>> validate_ptext(u'\u000B') #doctest: +ELLIPSIS
    Traceback (most recent call last):
    VCardFormatError: Invalid parameter value ...
    String: ...
    """
    if VALID_PTEXT.match(text) is None:
        raise VCardFormatError(MSG_INVALID_PARAM_VALUE, {'String': text})
예제 #7
0
def validate_uri(text):
    """
    genericurl, as described in RFC 1738
    <http://tools.ietf.org/html/rfc1738#section-5>.
    @param text: Single parameter value

    Examples:
    >>> validate_uri('http://example.org/')
    >>> validate_uri('http\\://example.org/') # doctest: +ELLIPSIS
    Traceback (most recent call last):
    VCardFormatError: Invalid URI ...
    String: http\\://example.org/
    >>> validate_uri('http:') # doctest: +ELLIPSIS
    Traceback (most recent call last):
    VCardFormatError: Invalid URI ...
    String: http:
    """
    parts = urlparse(text)
    if parts[0] == '' or (parts[1] == '' and parts[2] == ''):
        raise VCardFormatError(MSG_INVALID_URI, {'String': text})
예제 #8
0
def validate_quoted_string(text):
    """
    quoted-string, as described on page 28
    <http://tools.ietf.org/html/rfc2426#section-4>

    @param text: String

    Examples:
    >>> validate_quoted_string(DQUOTE_CHAR + QSAFE_CHARS[0] + DQUOTE_CHAR)
    >>> validate_quoted_string(DQUOTE_CHAR + DQUOTE_CHAR) # doctest: +ELLIPSIS
    Traceback (most recent call last):
    VCardFormatError: Invalid parameter value ...
    >>> validate_quoted_string(
    ... DQUOTE_CHAR + QSAFE_CHARS[-1]*2 + DQUOTE_CHAR) # doctest: +ELLIPSIS
    Traceback (most recent call last):
    VCardFormatError: Invalid parameter value ...
    String: "ÿÿ"
    """
    if VALID_QUOTED_STRING.match(text) is None:
        raise VCardFormatError(MSG_INVALID_PARAM_VALUE, {'String': text})
예제 #9
0
def validate_float(text):
    """
    float value, as described on page 10 of RFC 2425
    <http://tools.ietf.org/html/rfc2425#section-5.8.4>

    Examples:
    >>> validate_float('12')
    >>> validate_float('12.345')
    >>> validate_float('+12.345')
    >>> validate_float('-12.345')
    >>> validate_float('12.') # doctest: +ELLIPSIS
    Traceback (most recent call last):
        ...
    VCardFormatError: Invalid subvalue ...
    >>> validate_float('.12') # doctest: +ELLIPSIS
    Traceback (most recent call last):
        ...
    VCardFormatError: Invalid subvalue ...
    >>> validate_float('foo') # doctest: +ELLIPSIS
    Traceback (most recent call last):
        ...
    VCardFormatError: Invalid subvalue ...
    >>> validate_float('++12.345') # doctest: +ELLIPSIS
    Traceback (most recent call last):
        ...
    VCardFormatError: Invalid subvalue ...
    >>> validate_float('--12.345') # doctest: +ELLIPSIS
    Traceback (most recent call last):
        ...
    VCardFormatError: Invalid subvalue ...
    >>> validate_float('12.34.5') # doctest: +ELLIPSIS
    Traceback (most recent call last):
        ...
    VCardFormatError: Invalid subvalue ...
    """
    if VALID_FLOAT.match(text) is None:
        raise VCardFormatError(
            '{0}, expected float value: {1}'.format(MSG_INVALID_SUBVALUE,
                                                    text), {})
예제 #10
0
def validate_param_value(text):
    """
    param-value, as described on page 28
    <http://tools.ietf.org/html/rfc2426#section-4>

    @param text: Single parameter value

    Examples:
    >>> validate_param_value('')
    >>> validate_param_value(SAFE_CHARS)
    >>> validate_param_value(DQUOTE_CHAR + QSAFE_CHARS[0] + DQUOTE_CHAR)
    >>> validate_param_value(DQUOTE_CHAR + DQUOTE_CHAR) # doctest: +ELLIPSIS
    Traceback (most recent call last):
    VCardFormatError: Invalid parameter value ...
    String: ""
    """
    try:
        validate_ptext(text)
    except VCardFormatError:
        try:
            validate_quoted_string(text)
        except VCardFormatError:
            raise VCardFormatError(MSG_INVALID_PARAM_VALUE, {'String': text})
예제 #11
0
def validate_text_value(text):
    """
    text-value, as described on page 37
    <http://tools.ietf.org/html/rfc2426#section-4>

    @param text: String

    Examples:
    >>> validate_text_value('')
    >>> validate_text_value('\\\\,')
    >>> validate_text_value(SAFE_CHARS)
    >>> validate_text_value('\\\\n')
    >>> validate_text_value(';') # doctest: +ELLIPSIS
    Traceback (most recent call last):
    VCardFormatError: Invalid text value (See RFC 2426 section 4 for details)
    String: ...
    >>> validate_text_value('\\\\\\\\;') # doctest: +ELLIPSIS
    Traceback (most recent call last):
    VCardFormatError: Invalid text value (See RFC 2426 section 4 for details)
    String: ...
    """
    if VALID_TEXT.match(text) is None:
        raise VCardFormatError(MSG_INVALID_TEXT_VALUE, {'String': text})
예제 #12
0
def validate_language_tag(text):
    """
    langval, as defined by RFC 1766 <http://tools.ietf.org/html/rfc1766>

    @param text: String

    Examples:
    >>> validate_language_tag('en')
    >>> validate_language_tag('-US') # Need primary tag
    Traceback (most recent call last):
    VCardFormatError: Invalid language (See RFC 1766 section 2 for details)
    String: -us
    >>> validate_language_tag('en-') # Can't end with dash
    Traceback (most recent call last):
    VCardFormatError: Invalid language (See RFC 1766 section 2 for details)
    String: en-
    >>> validate_language_tag('en-US')

    """
    text = text.lower()  # Case insensitive

    if VALID_LANG_TAG.match(text) is None:
        raise VCardFormatError(MSG_INVALID_LANGUAGE_VALUE, {'String': text})
예제 #13
0
def _expect_subvalue_count(subvalues, count):
    if len(subvalues) != count:
        raise VCardFormatError(
            '{0}: {1:d} (expected {2})'.format(MSG_INVALID_SUBVALUE_COUNT,
                                               len(subvalues), count), {})
예제 #14
0
def _expect_params(prop):
    if not 'parameters' in prop:
        raise VCardFormatError(MSG_MISSING_PARAM, {})
예제 #15
0
def _expect_no_params(prop):
    if 'parameters' in prop:
        raise VCardFormatError(
            '{0}: {1[parameters]}'.format(MSG_NON_EMPTY_PARAM, prop), {})
예제 #16
0
def validate_vcard_property(prop):
    """
    Checks any property according to
    <http://tools.ietf.org/html/rfc2426#section-3> and
    <http://tools.ietf.org/html/rfc2426#section-4>. Checks are grouped by
    property to allow easy overview rather than a short function.

    @param prop: Formatted property
    """
    property_name = prop['name'].upper()

    try:
        if property_name in ('BEGIN', 'END'):
            # <http://tools.ietf.org/html/rfc2426#section-2.1.1>
            _expect_no_params(prop)
            _expect_value_count(prop['values'], 1)
            _expect_subvalue_count(prop['values'][0], 1)
            if prop['values'][0][0].lower() != 'vcard':
                raise VCardFormatError(
                    '{0}: {1} (expected "VCARD")'.format(
                        MSG_INVALID_VALUE, prop['values'][0][0]), {})

        if property_name == 'NAME':
            # <http://tools.ietf.org/html/rfc2426#section-2.1.2>
            _expect_no_params(prop)
            _expect_value_count(prop['values'], 1)
            _expect_subvalue_count(prop['values'][0], 1)
            validate_text_value(prop['values'][0][0])

        if property_name == 'PROFILE':
            # <http://tools.ietf.org/html/rfc2426#section-2.1.3>
            _expect_no_params(prop)
            _expect_value_count(prop['values'], 1)
            _expect_subvalue_count(prop['values'][0], 1)
            if prop['values'][0][0].lower() != 'vcard':
                raise VCardFormatError(
                    '{0}: {1} (expected "VCARD")'.format(
                        MSG_INVALID_VALUE, prop['values'][0][0]), {})
            validate_text_value(prop['values'][0][0])

        if property_name == 'SOURCE':
            # <http://tools.ietf.org/html/rfc2426#section-2.1.4>
            _expect_params(prop)
            for param_name, param_values in prop['parameters'].items():
                if param_name.upper() == 'VALUE':
                    if param_values != {'uri'}:
                        raise VCardFormatError(
                            '{0}: {1}'.format(MSG_INVALID_PARAM_VALUE,
                                              param_values), {})
                    if 'CONTEXT' in prop['parameters']:
                        raise VCardFormatError(
                            '{0}: {1} and {2}'.format(MSG_MISMATCH_PARAM,
                                                      ('VALUE', 'CONTEXT')),
                            {})
                elif param_name.upper() == 'CONTEXT':
                    if param_values != {'word'}:
                        raise VCardFormatError(
                            '{0}: {1}'.format(MSG_INVALID_PARAM_VALUE,
                                              param_values), {})
                    if 'VALUE' in prop['parameters']:
                        raise VCardFormatError(
                            '{0}: {1} and {2}'.format(MSG_MISMATCH_PARAM,
                                                      ('VALUE', 'CONTEXT')),
                            {})
                else:
                    raise VCardFormatError(
                        '{0}: {1}'.format(MSG_INVALID_PARAM_NAME, param_name),
                        {})

        if property_name == 'FN':
            # <http://tools.ietf.org/html/rfc2426#section-3.1.1>
            if 'parameters' in prop:
                for parameter in prop['parameters'].items():
                    validate_text_parameter(parameter)
            _expect_value_count(prop['values'], 1)
            _expect_subvalue_count(prop['values'][0], 1)
            validate_text_value(prop['values'][0][0])

        elif property_name == 'VERSION':
            _expect_no_params(prop)
            _expect_value_count(prop['values'], 1)
            if prop['values'][0][0] != '3.0':
                raise VCardFormatError(
                    '{0}: {1} (expected "3.0")'.format(MSG_INVALID_VALUE,
                                                       prop['values'][0][0]),
                    {})
        elif property_name == 'N':
            # <http://tools.ietf.org/html/rfc2426#section-3.1.2>
            if 'parameters' in prop:
                for parameter in prop['parameters'].items():
                    validate_text_parameter(parameter)
            _expect_value_count(prop['values'], 5)
            # Should names be split?
            for names in prop['values']:
                warnings.showwarning = show_warning
                for name in names:
                    validate_text_value(name)
                    if name.find(SP_CHAR) != -1 and ''.join(
                        [''.join(names) for names in prop['values']]) != name:
                        # Space in name
                        # Not just a single name
                        warnings.warn('{0}: {1}'.format(
                            WARN_MULTIPLE_NAMES, name.encode('utf-8')))

        elif property_name == 'NICKNAME':
            # <http://tools.ietf.org/html/rfc2426#section-3.1.3>
            if 'parameters' in prop:
                for parameter in prop['parameters'].items():
                    validate_text_parameter(parameter)
            _expect_value_count(prop['values'], 1)

        elif property_name in ['PHOTO', 'LOGO']:
            # <http://tools.ietf.org/html/rfc2426#section-3.1.4>
            # <http://tools.ietf.org/html/rfc2426#section-3.5.4>
            _expect_params(prop)
            _expect_value_count(prop['values'], 1)
            _expect_subvalue_count(prop['values'][0], 1)
            for param_name, param_values in prop['parameters'].items():
                if param_name.upper() == 'ENCODING':
                    if param_values != {'b'}:
                        raise VCardFormatError(
                            '{0}: {1}'.format(MSG_INVALID_PARAM_VALUE,
                                              param_values), {})
                    if 'VALUE' in prop['parameters']:
                        raise VCardFormatError(
                            '{0}: {1} and {2}'.format(MSG_MISMATCH_PARAM,
                                                      ('ENCODING', 'VALUE')),
                            {})
                elif param_name.upper(
                ) == 'TYPE' and 'ENCODING' not in prop['parameters']:
                    raise VCardFormatError(
                        '{0}: {1}'.format(MSG_MISSING_PARAM, 'ENCODING'), {})
                elif param_name.upper() == 'VALUE':
                    if param_values != {'uri'}:
                        raise VCardFormatError(
                            '{0}: {1}'.format(MSG_INVALID_PARAM_VALUE,
                                              param_values), {})
                    else:
                        validate_uri(prop['values'][0][0])
                elif param_name.upper() not in ['ENCODING', 'TYPE', 'VALUE']:
                    raise VCardFormatError(
                        '{0}: {1}'.format(MSG_INVALID_PARAM_NAME, param_name),
                        {})

        elif property_name == 'BDAY':
            # <http://tools.ietf.org/html/rfc2426#section-3.1.5>
            _expect_no_params(prop)
            _expect_value_count(prop['values'], 1)
            _expect_subvalue_count(prop['values'][0], 1)
            validate_date(prop['values'][0][0])

        elif property_name == 'ADR':
            # <http://tools.ietf.org/html/rfc2426#section-3.2.1>
            _expect_value_count(prop['values'], 7)
            if 'parameters' in prop:
                for param_name, param_values in prop['parameters'].items():
                    if param_name.upper() == 'TYPE':
                        for param_subvalue in param_values:
                            if param_subvalue not in [
                                    'dom', 'intl', 'postal', 'parcel', 'home',
                                    'work', 'pref'
                            ]:
                                raise VCardFormatError(
                                    '{0}: {1}'.format(MSG_INVALID_PARAM_VALUE,
                                                      param_subvalue), {})
                        if param_values == {
                                'intl', 'postal', 'parcel', 'work'
                        }:
                            warnings.warn('{0}: {1}'.format(
                                WARN_DEFAULT_TYPE_VALUE, prop['values']))
                    else:
                        validate_text_parameter(prop)

        elif property_name == 'LABEL':
            # <http://tools.ietf.org/html/rfc2426#section-3.2.2>
            if 'parameters' in prop:
                for param_name, param_values in prop['parameters'].items():
                    if param_name.upper() == 'TYPE':
                        for param_subvalue in param_values:
                            if param_subvalue not in [
                                    'dom', 'intl', 'postal', 'parcel', 'home',
                                    'work', 'pref'
                            ]:
                                raise VCardFormatError(
                                    '{0}: {1}'.format(MSG_INVALID_PARAM_VALUE,
                                                      param_subvalue), {})
                        if param_values == {
                                'intl', 'postal', 'parcel', 'work'
                        }:
                            warnings.warn('{0}: {1}'.format(
                                WARN_DEFAULT_TYPE_VALUE, prop['values']))
                    else:
                        validate_text_parameter(prop)
            _expect_value_count(prop['values'], 1)
            _expect_subvalue_count(prop['values'][0], 1)
            validate_text_value(prop['values'][0][0])

        elif property_name == 'TEL':
            # <http://tools.ietf.org/html/rfc2426#section-3.3.1>
            if 'parameters' in prop:
                for param_name, param_values in prop['parameters'].items():
                    if param_name.upper() == 'TYPE':
                        for param_subvalue in param_values:
                            if param_subvalue.lower() not in [
                                    'home', 'msg', 'work', 'pref', 'voice',
                                    'fax', 'cell', 'video', 'pager', 'bbs',
                                    'modem', 'car', 'isdn', 'pcs'
                            ]:
                                raise VCardFormatError(
                                    '{0}: {1}'.format(MSG_INVALID_PARAM_VALUE,
                                                      param_subvalue), {})
                        if set([value.lower()
                                for value in param_values]) == {'voice'}:
                            warnings.warn('{0}: {1}'.format(
                                WARN_DEFAULT_TYPE_VALUE, prop['values']))
                    else:
                        raise VCardFormatError(
                            '{0}: {1}'.format(MSG_INVALID_PARAM_NAME,
                                              param_name), {})
            _expect_value_count(prop['values'], 1)
            _expect_subvalue_count(prop['values'][0], 1)

        elif property_name == 'EMAIL':
            # <http://tools.ietf.org/html/rfc2426#section-3.3.2>
            if 'parameters' in prop:
                for param_name, param_values in prop['parameters'].items():
                    if param_name.upper() == 'TYPE':
                        for param_subvalue in param_values:
                            if param_subvalue.lower() not in [
                                    'internet',
                                    'x400',
                                    'pref',
                                    'dom',  # IANA registered address types?
                                    'intl',
                                    'postal',
                                    'parcel',
                                    'home',
                                    'work'
                            ]:
                                warnings.warn('{0}: {1}'.format(
                                    WARN_INVALID_EMAIL_TYPE, param_subvalue))
                        if set([value.lower() for value in param_values]) == \
                                {'internet'}:
                            warnings.warn('{0}: {1[values]}'.format(
                                WARN_DEFAULT_TYPE_VALUE, prop))
                    else:
                        raise VCardFormatError(
                            '{0}: {1}'.format(MSG_INVALID_PARAM_NAME,
                                              param_name), {})
            _expect_value_count(prop['values'], 1)
            _expect_subvalue_count(prop['values'][0], 1)
            validate_text_value(prop['values'][0][0])

        elif property_name == 'MAILER':
            # <http://tools.ietf.org/html/rfc2426#section-3.3.3>
            _expect_no_params(prop)
            _expect_value_count(prop['values'], 1)
            _expect_value_count(prop['values'][0], 1)
            validate_text_value(prop['values'][0][0])

        elif property_name == 'TZ':
            # <http://tools.ietf.org/html/rfc2426#section-3.4.1>
            _expect_no_params(prop)
            _expect_value_count(prop['values'], 1)
            _expect_subvalue_count(prop['values'][0], 1)
            value = prop['values'][0][0]
            validate_time_zone(value)

        elif property_name == 'GEO':
            # <http://tools.ietf.org/html/rfc2426#section-3.4.2>
            _expect_no_params(prop)
            _expect_value_count(prop['values'], 2)
            # can the following be...
            #   _expect_subvalue_count(prop['values'][0], 2)
            # ...?
            for value in prop['values']:
                if len(value) != 1:
                    raise VCardFormatError(
                        '{0}: {1:d} (expected 1)'.format(
                            MSG_INVALID_SUBVALUE_COUNT,
                            len(prop['values'][0])), {})
                validate_float(value[0])

        elif property_name == 'TITLE':
            # <http://tools.ietf.org/html/rfc2426#section-3.5.1>
            if 'parameters' in prop:
                for parameter in prop['parameters'].items():
                    validate_text_parameter(parameter)
            _expect_value_count(prop['values'], 1)
            _expect_subvalue_count(prop['values'][0], 1)
            validate_text_value(prop['values'][0][0])

        elif property_name == 'ROLE':
            # <http://tools.ietf.org/html/rfc2426#section-3.5.2>
            if 'parameters' in prop:
                for parameter in prop['parameters'].items():
                    validate_text_parameter(parameter)
            _expect_value_count(prop['values'], 1)
            _expect_subvalue_count(prop['values'][0], 1)
            validate_text_value(prop['values'][0][0])

        elif property_name == 'AGENT':
            # <http://tools.ietf.org/html/rfc2426#section-3.5.4>
            if 'parameters' in prop:
                for param_name, param_values in prop['parameters'].items():
                    if param_name.upper() != 'VALUE':
                        raise VCardFormatError(
                            '{0}: {1}'.format(MSG_INVALID_PARAM_NAME,
                                              param_values), {})
                    if param_values != {'uri'}:
                        raise VCardFormatError(
                            '{0}: {1}'.format(MSG_INVALID_PARAM_VALUE,
                                              param_values), {})
                _expect_value_count(prop['values'], 1)
                # can this be...
                #   _expect_subvalue_count(prop['values'][0], 1)
                # ...?
                for value in prop['values']:
                    if len(value) != 1:
                        raise VCardFormatError(
                            '{0}: {1:d} (expected 1)'.format(
                                MSG_INVALID_SUBVALUE_COUNT,
                                len(prop['values'][0])), {})
                    validate_uri(value[0])
            else:
                # Inline vCard object
                pass  # TODO: Un-escape and validate value

        elif property_name == 'URL':
            # <http://tools.ietf.org/html/rfc2426#section-3.6.8>
            _expect_no_params(prop)
            _expect_value_count(prop['values'], 1)
            validate_uri(prop['values'][0][0])

    except VCardFormatError as error:
        error.context['Property'] = property_name
        raise VCardFormatError(error.message, error.context)