mirror of
https://github.com/the-djmaze/snappymail.git
synced 2025-01-30 10:37:54 +08:00
Add some basic support for Linked Data (Structured Email) #1422
This commit is contained in:
parent
ea07ea45b8
commit
be633f78a7
5 changed files with 322 additions and 3 deletions
|
@ -1,5 +1,5 @@
|
|||
import { createElement } from 'Common/Globals';
|
||||
import { forEachObjectEntry, pInt } from 'Common/Utils';
|
||||
import { forEachObjectEntry, isArray, pInt } from 'Common/Utils';
|
||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||
|
||||
const
|
||||
|
@ -207,7 +207,8 @@ export const
|
|||
bqLevel = parseInt(SettingsUserStore.maxBlockquotesLevel()),
|
||||
|
||||
result = {
|
||||
hasExternals: false
|
||||
hasExternals: false,
|
||||
linkedData: []
|
||||
},
|
||||
|
||||
findAttachmentByCid = cId => oAttachments.findByCid(cId),
|
||||
|
@ -269,6 +270,7 @@ export const
|
|||
// Not supported by <template> element
|
||||
// .replace(/<!doctype[^>]*>/gi, '')
|
||||
// .replace(/<\?xml[^>]*\?>/gi, '')
|
||||
.replace(/<(\/?)head(\s[^>]*)?>/gi, '')
|
||||
.replace(/<(\/?)body(\s[^>]*)?>/gi, '<$1div class="mail-body"$2>')
|
||||
// .replace(/<\/?(html|head)[^>]*>/gi, '')
|
||||
// Fix Reddit https://github.com/the-djmaze/snappymail/issues/540
|
||||
|
@ -284,6 +286,21 @@ export const
|
|||
nodeIterator.referenceNode.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic support for Linked Data (Structured Email)
|
||||
* https://json-ld.org/
|
||||
* https://structured.email/
|
||||
**/
|
||||
tmpl.content.querySelectorAll('script[type="application/ld+json"]').forEach(oElement => {
|
||||
// Could be array of objects or single object
|
||||
try {
|
||||
const data = JSON.parse(oElement.textContent);
|
||||
(isArray(data) ? data : [data]).forEach(entry => result.linkedData.push(entry));
|
||||
} catch (e) {
|
||||
console.error(e, oElement.textContent);
|
||||
}
|
||||
});
|
||||
|
||||
tmpl.content.querySelectorAll(
|
||||
disallowedTags
|
||||
+ (0 < bqLevel ? ',' + (new Array(1 + bqLevel).fill('blockquote').join(' ')) : '')
|
||||
|
|
|
@ -87,6 +87,12 @@ export class MessageModel extends AbstractModel {
|
|||
size: 0,
|
||||
readReceipt: '',
|
||||
preview: null,
|
||||
/**
|
||||
* Basic support for Linked Data (Structured Email)
|
||||
* https://json-ld.org/
|
||||
* https://structured.email/
|
||||
**/
|
||||
linkedData: [],
|
||||
|
||||
attachments: ko.observableArray(new AttachmentCollectionModel),
|
||||
threads: ko.observableArray(),
|
||||
|
@ -367,6 +373,7 @@ export class MessageModel extends AbstractModel {
|
|||
let result = msgHtml(this);
|
||||
this.hasExternals(result.hasExternals);
|
||||
this.hasImages(!!result.hasExternals);
|
||||
this.linkedData = result.linkedData;
|
||||
body.innerHTML = result.html;
|
||||
if (!this.isSpam && FolderUserStore.spamFolder() != this.folder) {
|
||||
if ('always' === SettingsUserStore.viewImages()) {
|
||||
|
|
|
@ -1415,7 +1415,13 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
// Only used at send, not at save:
|
||||
dsn: this.requestDsn() ? 1 : 0,
|
||||
requireTLS: this.requireTLS() ? 1 : 0,
|
||||
readReceiptRequest: this.requestReadReceipt() ? 1 : 0
|
||||
readReceiptRequest: this.requestReadReceipt() ? 1 : 0,
|
||||
/**
|
||||
* Basic support for Linked Data (Structured Email)
|
||||
* https://json-ld.org/
|
||||
* https://structured.email/
|
||||
**/
|
||||
linkedData: []
|
||||
},
|
||||
recipients = draft ? [identity.email()] : this.allRecipients(),
|
||||
sign = !draft && this.pgpSign() && this.canPgpSign(),
|
||||
|
|
283
examples/eml/json-ld.eml
Normal file
283
examples/eml/json-ld.eml
Normal file
|
@ -0,0 +1,283 @@
|
|||
Return-Path: <json-ld@example.com>
|
||||
Delivered-To: json-ld@example.com
|
||||
MIME-Version: 1.0
|
||||
Date: Mon, 05 Feb 2024 12:37:15 +0000
|
||||
Content-Type: multipart/alternative;
|
||||
boundary="46987ad0-df2c-4cc8-8c7b-20e34b1f9bbf-1"
|
||||
From: json-ld@example.com
|
||||
Message-ID: <2cb0cdd30d1a7ec8c810710da8dc14eedaf10e7e@example.com>
|
||||
TLS-Required: No
|
||||
Subject: test json-ld
|
||||
To: json-ld@example.com
|
||||
|
||||
--46987ad0-df2c-4cc8-8c7b-20e34b1f9bbf-1
|
||||
Content-Type: text/plain; charset="utf-8"
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
test json-ld
|
||||
--46987ad0-df2c-4cc8-8c7b-20e34b1f9bbf-1
|
||||
Content-Type: text/html; charset="utf-8"
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
<!DOCTYPE html><html><head><meta http-equiv=3D"Content-Type" content=3D"t=
|
||||
ext/html; charset=3Dutf-8">
|
||||
<script type=3D"application/ld+json">
|
||||
[
|
||||
{
|
||||
"@context": "http://schema.org/",
|
||||
"@type": "Person",
|
||||
"name": "Jane Doe",
|
||||
"jobTitle": "Professor",
|
||||
"telephone": "(425) 123-4567",
|
||||
"url": "http://www.janedoe.com"
|
||||
},
|
||||
{
|
||||
"@context": {
|
||||
"ical": "http://www.w3.org/2002/12/cal/ical#",
|
||||
"xsd": "http://www.w3.org/2001/XMLSchema#",
|
||||
"ical:dtstart": {
|
||||
"@type": "xsd:dateTime"
|
||||
}
|
||||
},
|
||||
"ical:summary": "Lady Gaga Concert",
|
||||
"ical:location": "New Orleans Arena, New Orleans, Louisiana, USA",
|
||||
"ical:dtstart": "2011-04-09T20:00:00Z"
|
||||
},
|
||||
{
|
||||
"@context": {
|
||||
"name": "http://schema.org/name",
|
||||
"description": "http://schema.org/description",
|
||||
"image": {
|
||||
"@id": "http://schema.org/image",
|
||||
"@type": "@id"
|
||||
},
|
||||
"geo": "http://schema.org/geo",
|
||||
"latitude": {
|
||||
"@id": "http://schema.org/latitude",
|
||||
"@type": "xsd:float"
|
||||
},
|
||||
"longitude": {
|
||||
"@id": "http://schema.org/longitude",
|
||||
"@type": "xsd:float"
|
||||
},
|
||||
"xsd": "http://www.w3.org/2001/XMLSchema#"
|
||||
},
|
||||
"name": "The Empire State Building",
|
||||
"description": "The Empire State Building is a 102-story landmark in New York City.",
|
||||
"image": "http://www.civil.usherbrooke.ca/cours/gci215a/empire-state-building.jpg",
|
||||
"geo": {
|
||||
"latitude": "40.75",
|
||||
"longitude": "73.98"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@context": {
|
||||
"gr": "http://purl.org/goodrelations/v1#",
|
||||
"pto": "http://www.productontology.org/id/",
|
||||
"foaf": "http://xmlns.com/foaf/0.1/",
|
||||
"xsd": "http://www.w3.org/2001/XMLSchema#",
|
||||
"foaf:page": {
|
||||
"@type": "@id"
|
||||
},
|
||||
"gr:acceptedPaymentMethods": {
|
||||
"@type": "@id"
|
||||
},
|
||||
"gr:hasBusinessFunction": {
|
||||
"@type": "@id"
|
||||
},
|
||||
"gr:hasCurrencyValue": {
|
||||
"@type": "xsd:float"
|
||||
}
|
||||
},
|
||||
"@id": "http://example.org/cars/for-sale#tesla",
|
||||
"@type": "gr:Offering",
|
||||
"gr:name": "Used Tesla Roadster",
|
||||
"gr:description": "Need to sell fast and furiously",
|
||||
"gr:hasBusinessFunction": "gr:Sell",
|
||||
"gr:acceptedPaymentMethods": "gr:Cash",
|
||||
"gr:hasPriceSpecification": {
|
||||
"gr:hasCurrencyValue": "85000",
|
||||
"gr:hasCurrency": "USD"
|
||||
},
|
||||
"gr:includes": {
|
||||
"@type": [
|
||||
"gr:Individual",
|
||||
"pto:Vehicle"
|
||||
],
|
||||
"gr:name": "Tesla Roadster",
|
||||
"foaf:page": "http://www.teslamotors.com/roadster"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@context": {
|
||||
"name": "http://rdf.data-vocabulary.org/#name",
|
||||
"ingredient": "http://rdf.data-vocabulary.org/#ingredients",
|
||||
"yield": "http://rdf.data-vocabulary.org/#yield",
|
||||
"instructions": "http://rdf.data-vocabulary.org/#instructions",
|
||||
"step": {
|
||||
"@id": "http://rdf.data-vocabulary.org/#step",
|
||||
"@type": "xsd:integer"
|
||||
},
|
||||
"description": "http://rdf.data-vocabulary.org/#description",
|
||||
"xsd": "http://www.w3.org/2001/XMLSchema#"
|
||||
},
|
||||
"name": "Mojito",
|
||||
"ingredient": [
|
||||
"12 fresh mint leaves",
|
||||
"1/2 lime, juiced with pulp",
|
||||
"1 tablespoons white sugar",
|
||||
"1 cup ice cubes",
|
||||
"2 fluid ounces white rum",
|
||||
"1/2 cup club soda"
|
||||
],
|
||||
"yield": "1 cocktail",
|
||||
"instructions": [
|
||||
{
|
||||
"step": 1,
|
||||
"description": "Crush lime juice, mint and sugar together in glass."
|
||||
},
|
||||
{
|
||||
"step": 2,
|
||||
"description": "Fill glass to top with ice cubes."
|
||||
},
|
||||
{
|
||||
"step": 3,
|
||||
"description": "Pour white rum over ice."
|
||||
},
|
||||
{
|
||||
"step": 4,
|
||||
"description": "Fill the rest of glass with club soda, stir."
|
||||
},
|
||||
{
|
||||
"step": 5,
|
||||
"description": "Garnish with a lime wedge."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"@context": {
|
||||
"dc11": "http://purl.org/dc/elements/1.1/",
|
||||
"ex": "http://example.org/vocab#",
|
||||
"xsd": "http://www.w3.org/2001/XMLSchema#",
|
||||
"ex:contains": {
|
||||
"@type": "@id"
|
||||
}
|
||||
},
|
||||
"@graph": [
|
||||
{
|
||||
"@id": "http://example.org/library",
|
||||
"@type": "ex:Library",
|
||||
"ex:contains": "http://example.org/library/the-republic"
|
||||
},
|
||||
{
|
||||
"@id": "http://example.org/library/the-republic",
|
||||
"@type": "ex:Book",
|
||||
"dc11:creator": "Plato",
|
||||
"dc11:title": "The Republic",
|
||||
"ex:contains": "http://example.org/library/the-republic#introduction"
|
||||
},
|
||||
{
|
||||
"@id": "http://example.org/library/the-republic#introduction",
|
||||
"@type": "ex:Chapter",
|
||||
"dc11:description": "An introductory chapter on The Republic.",
|
||||
"dc11:title": "The Introduction"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"@type": "Create",
|
||||
"actor": {
|
||||
"@type": "Person",
|
||||
"@id": "acct:sally@example.org",
|
||||
"name": "Sally"
|
||||
},
|
||||
"object": {
|
||||
"@type": "Note",
|
||||
"content": "This is a simple note"
|
||||
},
|
||||
"published": "2015-01-25T12:34:56Z"
|
||||
},
|
||||
{
|
||||
"@context": "http://schema.org",
|
||||
"@type": "FoodEstablishmentReservation",
|
||||
"reservationNumber": "OT12345",
|
||||
"reservationStatus": "http://schema.org/Confirmed",
|
||||
"underName": {
|
||||
"@type": "Person",
|
||||
"name": "John Smith"
|
||||
},
|
||||
"reservationFor": {
|
||||
"@type": "FoodEstablishment",
|
||||
"name": "Wagamama",
|
||||
"address": {
|
||||
"@type": "PostalAddress",
|
||||
"streetAddress": "1 Tavistock Street",
|
||||
"addressLocality": "London",
|
||||
"addressRegion": "Greater London",
|
||||
"postalCode": "WC2E 7PG",
|
||||
"addressCountry": "United Kingdom"
|
||||
}
|
||||
},
|
||||
"startTime": "2027-04-10T08:00:00+00:00",
|
||||
"partySize": "2"
|
||||
},
|
||||
{
|
||||
"@context": "http://schema.org",
|
||||
"@type": "ParcelDelivery",
|
||||
"deliveryAddress": {
|
||||
"@type": "PostalAddress",
|
||||
"streetAddress": "757 Westwood Plaza",
|
||||
"addressLocality": "Los Angeles",
|
||||
"addressRegion": "CA",
|
||||
"addressCountry": "US",
|
||||
"postalCode": "90095"
|
||||
},
|
||||
"expectedArrivalUntil": "2016-11-12T12:00:00-09:00",
|
||||
"carrier": {
|
||||
"@type": "Organization",
|
||||
"name": "FedEx"
|
||||
},
|
||||
"itemShipped": {
|
||||
"@type": "Product",
|
||||
"name": "A bottle of beer"
|
||||
},
|
||||
"partOfOrder": {
|
||||
"@type": "Order",
|
||||
"orderNumber": "159487-sh-1971",
|
||||
"merchant": {
|
||||
"@type": "Organization",
|
||||
"name": "SomeRetailer.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
</script>
|
||||
<script type=3D"application/ld+json">
|
||||
[
|
||||
{
|
||||
"@context": "http://schema.org/",
|
||||
"@type": "PromotionCard",
|
||||
"image": "IMAGE_URL1",
|
||||
"url": "PROMO_URL1",
|
||||
"headline": "HEADLINE1",
|
||||
"price": "PRICE1",
|
||||
"priceCurrency": "PRICE_CURRENCY1",
|
||||
"discountValue": "DISCOUNT_VALUE1"
|
||||
},
|
||||
{
|
||||
"@context": "http://schema.org/",
|
||||
"@type": "PromotionCard",
|
||||
"image": "IMAGE_URL2",
|
||||
"url": "PROMO_URL2",
|
||||
"headline": "HEADLINE2",
|
||||
"price": "PRICE2",
|
||||
"priceCurrency": "PRICE_CURRENCY2",
|
||||
"discountValue": "DISCOUNT_VALUE2"
|
||||
}
|
||||
]
|
||||
</script></head><body>
|
||||
Example of Linked Data (Structured Email)
|
||||
|
||||
</body></html>
|
||||
--46987ad0-df2c-4cc8-8c7b-20e34b1f9bbf-1--
|
|
@ -1066,6 +1066,12 @@ trait Messages
|
|||
$oMessage->SubParts->append($oPart);
|
||||
|
||||
$sHtml = \MailSo\Base\HtmlUtils::BuildHtml($sHtml, $aFoundCids, $aFoundDataURL, $aFoundContentLocationUrls);
|
||||
|
||||
$aLinkedData = $this->GetActionParam('linkedData', []);
|
||||
if ($aLinkedData) {
|
||||
$sHtml = \str_replace('</head>', '<script type="application/ld+json">'.\json_encode($aLinkedData).'</script></head>', $sHtml);
|
||||
}
|
||||
|
||||
$this->Plugins()->RunHook('filter.message-html', array($oAccount, $oMessage, &$sHtml));
|
||||
|
||||
// First add plain
|
||||
|
|
Loading…
Reference in a new issue