From 76cf161c12644c51a5d22aade237711621ec7e19 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Wed, 5 Aug 2026 14:39:44 -0300 Subject: [PATCH] Give email addresses their mailto identity Signed-off-by: Juan Cruz Viotti --- examples/contact-card.json | 38 +++ package-lock.json | 8 +- package.json | 2 +- schemas/ietf/email/5321/address/v1.json | 1 + test/examples/contact-card.test.json | 87 +++++++ .../ietf/email/5321/address/v1.test.json | 245 +++++++++++++++--- 6 files changed, 341 insertions(+), 40 deletions(-) create mode 100644 examples/contact-card.json create mode 100644 test/examples/contact-card.test.json diff --git a/examples/contact-card.json b/examples/contact-card.json new file mode 100644 index 00000000..cc6d20ed --- /dev/null +++ b/examples/contact-card.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Contact Card", + "description": "A personal contact entry that identifies the person by their mailbox as a mailto IRI, their federated account as an acct IRI, and their website, all minted from the standard library", + "examples": [ + { + "name": "Grace Hopper", + "email": "grace.hopper@Example.COM", + "account": "grace@social.example.net", + "website": "https://example.com/~grace" + } + ], + "x-jsonld-type": "https://schema.org/Person", + "x-license": "https://github.com/sourcemeta/std/blob/main/LICENSE", + "x-links": [ "https://schema.org/Person" ], + "type": "object", + "required": [ "name", "email" ], + "properties": { + "name": { + "x-jsonld-id": "https://schema.org/name", + "type": "string" + }, + "email": { + "x-jsonld-id": "http://xmlns.com/foaf/0.1/mbox", + "$ref": "../schemas/ietf/email/5321/address/v1.json" + }, + "account": { + "x-jsonld-id": "https://schema.org/identifier", + "x-jsonld-self": "acct", + "type": "string" + }, + "website": { + "x-jsonld-id": "http://xmlns.com/foaf/0.1/homepage", + "$ref": "../schemas/ietf/uri/3986/uri/v1.json" + } + }, + "additionalProperties": false +} diff --git a/package-lock.json b/package-lock.json index 6f738193..194efcd9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,13 +8,13 @@ "name": "@sourcemeta/std", "version": "0.0.0", "devDependencies": { - "@sourcemeta/jsonschema": "^16.5.0" + "@sourcemeta/jsonschema": "^16.6.0" } }, "node_modules/@sourcemeta/jsonschema": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/@sourcemeta/jsonschema/-/jsonschema-16.5.0.tgz", - "integrity": "sha512-zyFKYEOGOdRSCH567BzaXycf4GPFxkIUjtLeN5jZqMXUqaKoiQAKDtJTo3km6Jyq/7FKL/FhR2SQgYW5UKE4PQ==", + "version": "16.6.0", + "resolved": "https://registry.npmjs.org/@sourcemeta/jsonschema/-/jsonschema-16.6.0.tgz", + "integrity": "sha512-gEJSsYYQkebdqtUos7ilITihCo4RmTWGIPg1kbMWaMfYFytjcUONIRklPXAhBmNX8crsEf1fQZpQuPXMo9HSkQ==", "cpu": [ "x64", "arm64" diff --git a/package.json b/package.json index 01f28ded..39cbbde7 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,6 @@ "version": "0.0.0", "private": true, "devDependencies": { - "@sourcemeta/jsonschema": "^16.5.0" + "@sourcemeta/jsonschema": "^16.6.0" } } diff --git a/schemas/ietf/email/5321/address/v1.json b/schemas/ietf/email/5321/address/v1.json index a2e5a839..7d92e866 100644 --- a/schemas/ietf/email/5321/address/v1.json +++ b/schemas/ietf/email/5321/address/v1.json @@ -8,6 +8,7 @@ "\"very.unusual.@.unusual.com\"@example.com", "user@[IPv6:2001:db8::1]" ], + "x-jsonld-self": "mailto", "x-license": "https://github.com/sourcemeta/std/blob/main/LICENSE", "x-links": [ "https://www.rfc-editor.org/rfc/rfc5321#section-4.1.2" ], "type": "string", diff --git a/test/examples/contact-card.test.json b/test/examples/contact-card.test.json new file mode 100644 index 00000000..0793e6c1 --- /dev/null +++ b/test/examples/contact-card.test.json @@ -0,0 +1,87 @@ +{ + "target": "../../examples/contact-card.json", + "tests": [ + { + "description": "Valid - complete card", + "data": { + "name": "Grace Hopper", + "email": "grace.hopper@Example.COM", + "account": "grace@social.example.net", + "website": "https://example.com/~grace" + }, + "valid": true, + "rdf": [ + { + "@type": [ "https://schema.org/Person" ], + "https://schema.org/identifier": [ + { + "@id": "acct:grace@social.example.net" + } + ], + "http://xmlns.com/foaf/0.1/mbox": [ + { + "@id": "mailto:grace.hopper@example.com" + } + ], + "https://schema.org/name": [ + { + "@value": "Grace Hopper" + } + ], + "http://xmlns.com/foaf/0.1/homepage": [ + { + "@id": "https://example.com/~grace" + } + ] + } + ] + }, + { + "description": "Valid - minimal card", + "data": { + "name": "Margaret Hamilton", + "email": "50%off@example.com" + }, + "valid": true, + "rdf": [ + { + "@type": [ "https://schema.org/Person" ], + "http://xmlns.com/foaf/0.1/mbox": [ + { + "@id": "mailto:50%25off@example.com" + } + ], + "https://schema.org/name": [ + { + "@value": "Margaret Hamilton" + } + ] + } + ] + }, + { + "description": "Invalid - missing email", + "data": { + "name": "Grace Hopper" + }, + "valid": false + }, + { + "description": "Invalid - email without a domain", + "data": { + "name": "Grace Hopper", + "email": "grace.hopper" + }, + "valid": false + }, + { + "description": "Invalid - unknown property", + "data": { + "name": "Grace Hopper", + "email": "grace.hopper@example.com", + "phone": "+15551234567" + }, + "valid": false + } + ] +} diff --git a/test/schemas/ietf/email/5321/address/v1.test.json b/test/schemas/ietf/email/5321/address/v1.test.json index 821ad87f..f4d4c4d3 100644 --- a/test/schemas/ietf/email/5321/address/v1.test.json +++ b/test/schemas/ietf/email/5321/address/v1.test.json @@ -9,122 +9,242 @@ { "description": "Basic email format", "data": "simple@example.com", - "valid": true + "valid": true, + "rdf": [ + { + "@id": "mailto:simple@example.com" + } + ] }, { "description": "Local-part with dot", "data": "very.common@example.com", - "valid": true + "valid": true, + "rdf": [ + { + "@id": "mailto:very.common@example.com" + } + ] }, { "description": "Mixed case local and domain", "data": "FirstName.LastName@EasierReading.org", - "valid": true + "valid": true, + "rdf": [ + { + "@id": "mailto:FirstName.LastName@easierreading.org" + } + ] }, { "description": "Single character local part", "data": "x@example.com", - "valid": true + "valid": true, + "rdf": [ + { + "@id": "mailto:x@example.com" + } + ] }, { "description": "Local-part with hyphens and subdomains", "data": "long.email-address-with-hyphens@and.subdomains.example.com", - "valid": true + "valid": true, + "rdf": [ + { + "@id": "mailto:long.email-address-with-hyphens@and.subdomains.example.com" + } + ] }, { "description": "Local-part with plus sign and tags", "data": "user.name+tag+sorting@example.com", - "valid": true + "valid": true, + "rdf": [ + { + "@id": "mailto:user.name+tag+sorting@example.com" + } + ] }, { "description": "Local-part with slash", "data": "name/surname@example.com", - "valid": true + "valid": true, + "rdf": [ + { + "@id": "mailto:name%2Fsurname@example.com" + } + ] }, { "description": "Local-part with underscore", "data": "user_name@example.com", - "valid": true + "valid": true, + "rdf": [ + { + "@id": "mailto:user_name@example.com" + } + ] }, { "description": "Local-part and domain with hyphen", "data": "user-name@sub-domain.example.co.uk", - "valid": true + "valid": true, + "rdf": [ + { + "@id": "mailto:user-name@sub-domain.example.co.uk" + } + ] }, { "description": "Quoted local-part with double dot inside", "data": "\"john..doe\"@example.org", - "valid": true + "valid": true, + "rdf": [ + { + "@id": "mailto:%22john..doe%22@example.org" + } + ] }, { "description": "Quoted local-part with space", "data": "\" \"@example.org", - "valid": true + "valid": true, + "rdf": [ + { + "@id": "mailto:%22%20%22@example.org" + } + ] }, { "description": "Quoted local-part ending with dot", "data": "\"john.doe.\"@example.com", - "valid": true + "valid": true, + "rdf": [ + { + "@id": "mailto:%22john.doe.%22@example.com" + } + ] }, { "description": "Quoted local-part containing @ symbol", "data": "\"john@doe\"@example.com", - "valid": true + "valid": true, + "rdf": [ + { + "@id": "mailto:%22john%40doe%22@example.com" + } + ] }, { "description": "Example with all allowed special characters in local-part", "data": "test!#$%&'*+/=?^_`{|}~@example.com", - "valid": true + "valid": true, + "rdf": [ + { + "@id": "mailto:test!%23$%25%26'*+%2F%3D%3F%5E_%60%7B%7C%7D~@example.com" + } + ] }, { "description": "Bang path style local-part", "data": "mailhost!username@example.org", - "valid": true + "valid": true, + "rdf": [ + { + "@id": "mailto:mailhost!username@example.org" + } + ] }, { "description": "Percent hack routing", "data": "user%example.com@example.org", - "valid": true + "valid": true, + "rdf": [ + { + "@id": "mailto:user%25example.com@example.org" + } + ] }, { "description": "Local-part ending with dash", "data": "user-@example.org", - "valid": true + "valid": true, + "rdf": [ + { + "@id": "mailto:user-@example.org" + } + ] }, { "description": "IPv4 address literal domain", "data": "postmaster@[123.123.123.123]", - "valid": true + "valid": true, + "rdf": [ + { + "@id": "mailto:postmaster@%5B123.123.123.123%5D" + } + ] }, { "description": "IPv6 address literal domain", "data": "postmaster@[IPv6:2001:db8::85a3:0:0:8a2e:370:7334]", - "valid": true + "valid": true, + "rdf": [ + { + "@id": "mailto:postmaster@%5BIPv6:2001:db8::85a3:0:0:8a2e:370:7334%5D" + } + ] }, { "description": "Local-part starts with underscore", "data": "_test123@example.org", - "valid": true + "valid": true, + "rdf": [ + { + "@id": "mailto:_test123@example.org" + } + ] }, { "description": "Plus in local-part with uppercase domain", "data": "test+filter@Example.COM", - "valid": true + "valid": true, + "rdf": [ + { + "@id": "mailto:test+filter@example.com" + } + ] }, { "description": "Dot in local-part", "data": "test.filter@example.com", - "valid": true + "valid": true, + "rdf": [ + { + "@id": "mailto:test.filter@example.com" + } + ] }, { "description": "Underscore in local-part", "data": "test_filter@example.com", - "valid": true + "valid": true, + "rdf": [ + { + "@id": "mailto:test_filter@example.com" + } + ] }, { "description": "Domain with no TLD (dotless domain)", "data": "admin@example", - "valid": true + "valid": true, + "rdf": [ + { + "@id": "mailto:admin@example" + } + ] }, { "description": "Comments around local, domain, and address", @@ -134,7 +254,12 @@ { "description": "Quoted local-part with space and letters", "data": "\"Fred Bloggs\"@example.com", - "valid": true + "valid": true, + "rdf": [ + { + "@id": "mailto:%22Fred%20Bloggs%22@example.com" + } + ] }, { "description": "Domain literal with letters and dot", @@ -144,27 +269,52 @@ { "description": "All uppercase local and domain", "data": "USER@EXAMPLE.COM", - "valid": true + "valid": true, + "rdf": [ + { + "@id": "mailto:USER@example.com" + } + ] }, { "description": "Local-part with tilde and domain with hyphen", "data": "a~b@c-domain.com", - "valid": true + "valid": true, + "rdf": [ + { + "@id": "mailto:a~b@c-domain.com" + } + ] }, { "description": "Local-part with slash and plus", "data": "first.last+category/department@domain.com", - "valid": true + "valid": true, + "rdf": [ + { + "@id": "mailto:first.last+category%2Fdepartment@domain.com" + } + ] }, { "description": "Local-part with apostrophe", "data": "o'reilly@example.com", - "valid": true + "valid": true, + "rdf": [ + { + "@id": "mailto:o'reilly@example.com" + } + ] }, { "description": "Local-part with multiple route characters", "data": "dizzy%example.com+tag@example.org", - "valid": true + "valid": true, + "rdf": [ + { + "@id": "mailto:dizzy%25example.com+tag@example.org" + } + ] }, { "description": "Email with comment in domain", @@ -179,7 +329,12 @@ { "description": "Complex quoted local-part with many allowed symbols", "data": "\"very.(),:;<>[]\\\".VERY.\\\"very@\\\\ \\\"very\\\".unusual\"@strange.example.com", - "valid": true + "valid": true, + "rdf": [ + { + "@id": "mailto:%22very.()%2C:%3B%3C%3E%5B%5D%5C%22.VERY.%5C%22very%40%5C%5C%20%5C%22very%5C%22.unusual%22@strange.example.com" + } + ] }, { "description": "Missing @ symbol", @@ -249,7 +404,12 @@ { "description": "Numeric TLD in domain", "data": "user@example.123", - "valid": true + "valid": true, + "rdf": [ + { + "@id": "mailto:user@example.123" + } + ] }, { "description": "Multiple @ symbols", @@ -329,12 +489,22 @@ { "description": "Uppercase numeric TLD", "data": "user@EXAMPLE.123", - "valid": true + "valid": true, + "rdf": [ + { + "@id": "mailto:user@example.123" + } + ] }, { "description": "IP address without brackets in domain", "data": "user@123.123.123.123", - "valid": true + "valid": true, + "rdf": [ + { + "@id": "mailto:user@123.123.123.123" + } + ] }, { "description": "IPv6 address without brackets", @@ -344,7 +514,12 @@ { "description": "Single quote in local part", "data": "'test'@example.com", - "valid": true + "valid": true, + "rdf": [ + { + "@id": "mailto:'test'@example.com" + } + ] }, { "description": "Double quotes misplacement",