diff --git a/packages/preview-server/package.json b/packages/preview-server/package.json index 9399396f2a..9e0030bcc7 100644 --- a/packages/preview-server/package.json +++ b/packages/preview-server/package.json @@ -4,7 +4,6 @@ "description": "A live preview of your emails right in your browser.", "scripts": { "build": "cross-env NODE_OPTIONS=\"--experimental-vm-modules --disable-warning=ExperimentalWarning\" tsx ./scripts/build-preview-server.mts", - "caniemail:fetch": "tsx ./scripts/fill-caniemail-data.mts", "clean": "rimraf dist", "dev": "cross-env NODE_OPTIONS=\"--experimental-vm-modules --disable-warning=ExperimentalWarning\" tsx ./scripts/dev.mts", "dev:seed": "tsx ./scripts/seed.mts", @@ -13,7 +12,6 @@ }, "main": "./index.mjs", "dependencies": { - "next": "16.1.5", "@babel/core": "7.26.10", "@babel/parser": "7.27.0", "@babel/traverse": "7.27.0", @@ -27,7 +25,6 @@ "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-toggle-group": "1.1.11", "@radix-ui/react-tooltip": "1.2.8", - "@react-email/tailwind": "workspace:2.0.4-canary.0", "@react-email/body": "workspace:*", "@react-email/button": "workspace:*", "@react-email/code-block": "workspace:*", @@ -38,6 +35,7 @@ "@react-email/img": "workspace:*", "@react-email/link": "workspace:*", "@react-email/preview": "workspace:*", + "@react-email/tailwind": "workspace:2.0.4-canary.0", "@react-email/text": "workspace:*", "@tailwindcss/postcss": "4.1.17", "clsx": "2.1.1", @@ -46,6 +44,7 @@ "framer-motion": "12.23.22", "log-symbols": "4.1.0", "module-punycode": "npm:punycode@2.3.1", + "next": "16.1.5", "next-safe-action": "8.0.11", "node-html-parser": "7.0.2", "ora": "5.4.1", @@ -54,6 +53,7 @@ "prism-react-renderer": "2.4.1", "react": "19.0.0", "react-dom": "19.0.0", + "react-email": "workspace:5.3.0-canary.2", "resend": "6.4.0", "socket.io-client": "4.8.3", "sonner": "2.0.7", @@ -68,10 +68,7 @@ "@react-email/components": "workspace:*", "@types/babel__core": "7.20.5", "@types/babel__traverse": "7.20.7", - "cross-env": "10.1.0", "@types/css-tree": "2.3.11", - "rimraf": "6.1.2", - "sharp": "0.34.5", "@types/fs-extra": "11.0.4", "@types/mime-types": "2.1.4", "@types/node": "22.19.7", @@ -79,6 +76,9 @@ "@types/react": "19.0.10", "@types/react-dom": "19.0.4", "@types/webpack": "5.28.5", + "cross-env": "10.1.0", + "rimraf": "6.1.2", + "sharp": "0.34.5", "typescript": "5.8.3" }, "license": "MIT", diff --git a/packages/preview-server/src/actions/email-validation/check-compatibility.ts b/packages/preview-server/src/actions/email-validation/check-compatibility.ts index 5852040d26..27e1f1754c 100644 --- a/packages/preview-server/src/actions/email-validation/check-compatibility.ts +++ b/packages/preview-server/src/actions/email-validation/check-compatibility.ts @@ -1,115 +1,27 @@ -'use server'; +/eleuse server'; import { parse } from '@babel/parser'; import traverse from '@babel/traverse'; import type { SourceLocation, StylePropertyUsage, -} from '../../utils/caniemail/ast/get-used-style-properties'; +} from '../../utils/ast/get-used-style-properties'; import { convertLocationIntoObject, doesPropertyHaveLocation, getUsedStyleProperties, -} from '../../utils/caniemail/ast/get-used-style-properties'; -import type { - CompatibilityStats, - SupportStatus, -} from '../../utils/caniemail/get-compatibility-stats-for-entry'; -import { getCompatibilityStatsForEntry } from '../../utils/caniemail/get-compatibility-stats-for-entry'; -import { getCssFunctions } from '../../utils/caniemail/get-css-functions'; -import { getCssPropertyNames } from '../../utils/caniemail/get-css-property-names'; -import { getCssPropertyWithValue } from '../../utils/caniemail/get-css-property-with-value'; -import { getCssUnit } from '../../utils/caniemail/get-css-unit'; -import { getElementAttributes } from '../../utils/caniemail/get-element-attributes'; -import { getElementNames } from '../../utils/caniemail/get-element-names'; +} from '../../utils/ast/get-used-style-properties'; +import { + type CompatibilityStats, + type SupportStatus, + type EmailClient, + checkSupportForAttribute, + checkSupportForElement, + checkSupportForStyleDeclaration, +} from 'react-email'; import { snakeToCamel } from '../../utils/snake-to-camel'; import { supportEntries } from './caniemail-data'; -export interface CompatibilityCheckingResult { - location: SourceLocation; - source: string; - entry: SupportEntry; - status: SupportStatus; - statsPerEmailClient: CompatibilityStats['perEmailClient']; -} - -export type EmailClient = - | 'gmail' - | 'outlook' - | 'yahoo' - | 'apple-mail' - | 'aol' - | 'thunderbird' - | 'microsoft' - | 'samsung-email' - | 'sfr' - | 'orange' - | 'protonmail' - | 'hey' - | 'mail-ru' - | 'fastmail' - | 'laposte' - | 't-online-de' - | 'free-fr' - | 'gmx' - | 'web-de' - | 'ionos-1and1' - | 'rainloop' - | 'wp-pl'; - -export type Platform = - | 'desktop-app' - | 'desktop-webmail' - | 'mobile-webmail' - | 'webmail' - | 'ios' - | 'android' - | 'windows' - | 'macos' - | 'windows-mail' - | 'outlook-com'; - -export type SupportEntryCategory = 'html' | 'css' | 'image' | 'others'; - -export interface SupportEntry { - slug: string; - title: string; - description: string | null; - url: string; - category: SupportEntryCategory; - tags: string[]; - keywords: string | null; - last_test_date: string; - test_url: string; - test_results_url: string | null; - stats: Partial< - Record< - EmailClient, - Partial< - Record< - Platform, - /* - This last Record has only one key, as the - ordered version of caniemail's data is meant to be something like: - - [ - { "1.0": "u" }, - { "2.0": "y" }, - { "3.0": "p #1" }, - ] - - So only one key for each object inside of this array, TypeScript can't really - describe this though AFAIK. - */ - Record[] - > - > - > - >; - notes: string | null; - notes_by_num: Record | null; -} - const relevantEmailClients: EmailClient[] = [ 'gmail', 'apple-mail', @@ -146,6 +58,23 @@ export const checkCompatibility = async ( ); const readableStream = new ReadableStream({ async start(controller) { + traverse(ast, { + JSXOpeningElement(path) { + if (path.node.name.type === 'JSXIdentifier' && path.node.name.loc) { + const elementName = path.node.name.name; + const stats = checkSupportForElement(elementName, relevantEmailClients); + if (stats.length > 0) { + controller.enqueue({ + source: getSourceCodeAt(path.node.name.loc), + location: convertLocationIntoObject(path.node.name.loc), + statsPerEmailClient: compatibilityStats.perEmailClient, + status: compatibilityStats.status, + }); + } + } + }, + }); + for (const entry of supportEntries) { const compatibilityStats = getCompatibilityStatsForEntry( entry, @@ -176,26 +105,6 @@ export const checkCompatibility = async ( let addedInsight = false; if (htmlEntryType === 'element') { - traverse(ast, { - JSXOpeningElement(path) { - if (path.node.name.type === 'JSXIdentifier' && !addedInsight) { - const elementName = path.node.name.name; - if ( - entryElements.includes(elementName) && - path.node.name.loc - ) { - addedInsight = true; - controller.enqueue({ - entry, - source: getSourceCodeAt(path.node.name.loc), - location: convertLocationIntoObject(path.node.name.loc), - statsPerEmailClient: compatibilityStats.perEmailClient, - status: compatibilityStats.status, - }); - } - } - }, - }); } else { traverse(ast, { JSXAttribute(path) { diff --git a/packages/preview-server/src/utils/caniemail/ast/__snapshots__/get-object-variables.spec.ts.snap b/packages/preview-server/src/utils/ast/__snapshots__/get-object-variables.spec.ts.snap similarity index 100% rename from packages/preview-server/src/utils/caniemail/ast/__snapshots__/get-object-variables.spec.ts.snap rename to packages/preview-server/src/utils/ast/__snapshots__/get-object-variables.spec.ts.snap diff --git a/packages/preview-server/src/utils/caniemail/ast/get-used-style-properties.spec.ts b/packages/preview-server/src/utils/ast/get-used-style-properties.spec.ts similarity index 100% rename from packages/preview-server/src/utils/caniemail/ast/get-used-style-properties.spec.ts rename to packages/preview-server/src/utils/ast/get-used-style-properties.spec.ts diff --git a/packages/preview-server/src/utils/caniemail/ast/get-used-style-properties.ts b/packages/preview-server/src/utils/ast/get-used-style-properties.ts similarity index 97% rename from packages/preview-server/src/utils/caniemail/ast/get-used-style-properties.ts rename to packages/preview-server/src/utils/ast/get-used-style-properties.ts index 5c49a154ec..30bcf52125 100644 --- a/packages/preview-server/src/utils/caniemail/ast/get-used-style-properties.ts +++ b/packages/preview-server/src/utils/ast/get-used-style-properties.ts @@ -1,8 +1,8 @@ import traverse, { type Node, type NodePath } from '@babel/traverse'; import { inlineStyles, sanitizeStyleSheet } from '@react-email/tailwind'; import type { StyleSheet } from 'css-tree'; -import type { AST } from '../../../actions/email-validation/check-compatibility'; -import { getTailwindMetadata } from '../tailwind/get-tailwind-metadata'; +import type { AST } from '../../actions/email-validation/check-compatibility'; +import { getTailwindMetadata } from '../caniemail/tailwind/get-tailwind-metadataata'; export interface StylePropertyUsage { location: SourceLocation | undefined | null; diff --git a/packages/preview-server/src/utils/caniemail/get-compatibility-stats-for-entry.ts b/packages/preview-server/src/utils/caniemail/get-compatibility-stats-for-entry.ts deleted file mode 100644 index e4e6a403b1..0000000000 --- a/packages/preview-server/src/utils/caniemail/get-compatibility-stats-for-entry.ts +++ /dev/null @@ -1,118 +0,0 @@ -import type { - EmailClient, - Platform, - SupportEntry, -} from '../../actions/email-validation/check-compatibility'; - -export type SupportStatus = DetailedSupportStatus['status']; - -export type DetailedSupportStatus = - | { - status: 'success'; - } - | { - status: 'error'; - } - | { - status: 'warning'; - notes: string; - }; - -type EmailClientStats = { - status: SupportStatus; - perPlatform: Partial>; -}; - -export type CompatibilityStats = { - status: SupportStatus; - perEmailClient: Partial>; -}; - -const noteNumbersRegex = /#(?\d+)/g; - -export const getCompatibilityStatsForEntry = ( - entry: SupportEntry, - emailClients: EmailClient[], -) => { - const stats: CompatibilityStats = { - status: 'success', - perEmailClient: {}, - }; - for (const emailClient of emailClients) { - const rawStats = entry.stats[emailClient]; - if (rawStats) { - const emailClientStats: EmailClientStats = { - status: 'success', - perPlatform: {}, - }; - - for (const [platform, statusPerVersion] of Object.entries(rawStats)) { - const latestStatus = statusPerVersion[statusPerVersion.length - 1]; - if (latestStatus === undefined) - throw new Error( - 'Cannot load in status because there are none recorded for this platform/email client', - { - cause: { - latestStatus, - statusPerVersion, - platform, - emailClient, - supportEntry: entry, - }, - }, - ); - const statusString = latestStatus[Object.keys(latestStatus)[0]!]!; - if (statusString.startsWith('u')) continue; - if (statusString.startsWith('a')) { - const notes: string[] = []; - noteNumbersRegex.lastIndex = 0; - for (const match of statusString.matchAll(noteNumbersRegex)) { - if (match.groups?.noteNumber) { - const { noteNumber } = match.groups; - const note = - entry.notes_by_num?.[Number.parseInt(noteNumber, 10)]; - if (note) { - notes.push(note); - } - // else if (isInternalDev) { - // console.warn( - // 'Could not get note by the number for a support entry', - // { - // platform, - // statusString, - // note, - // }, - // ); - // } - } - } - if (emailClientStats.status === 'success') - emailClientStats.status = 'warning'; - if (stats.status === 'success') stats.status = 'warning'; - emailClientStats.perPlatform[platform as Platform] = { - status: 'warning', - notes: - notes.length === 1 - ? notes[0]! - : notes.map((note) => `- ${note}`).join('\n'), - }; - } else if (statusString.startsWith('y')) { - emailClientStats.perPlatform[platform as Platform] = { - status: 'success', - }; - } else if (statusString.startsWith('n')) { - if (emailClientStats.status !== 'error') - emailClientStats.status = 'error'; - if (stats.status !== 'error') stats.status = 'error'; - emailClientStats.perPlatform[platform as Platform] = { - status: 'error', - }; - } - } - - stats.perEmailClient[emailClient] = emailClientStats; - } - } - - return stats; -}; diff --git a/packages/preview-server/src/utils/caniemail/get-css-functions.ts b/packages/preview-server/src/utils/caniemail/get-css-functions.ts deleted file mode 100644 index d5d03824f1..0000000000 --- a/packages/preview-server/src/utils/caniemail/get-css-functions.ts +++ /dev/null @@ -1,25 +0,0 @@ -export function getCssFunctions(title: string) { - if (/^[a-zA-Z]\(\)$/.test(title.trim())) { - return [title.replace('()', '')]; - } - - // ex: lch(), oklch(), lab(), oklab() - // this regex avoids matching entries that are for CSS properties listed - // separated by commas as well - if (/^(?:[^(),]+?\(\),?)*$/.test(title.trim())) { - return title - .split(/\s*,\s*/) - .map((functionCallWithoutParameters) => - functionCallWithoutParameters.replace('()', ''), - ); - } - - // ex: CSS calc() function - if (/^CSS [a-z]+\(\) function$/.test(title.trim())) { - return [ - title.replace('CSS ', '').replace(' function', '').replace('()', ''), - ]; - } - - return []; -} diff --git a/packages/preview-server/src/utils/caniemail/get-css-property-names.ts b/packages/preview-server/src/utils/caniemail/get-css-property-names.ts deleted file mode 100644 index 41867479e3..0000000000 --- a/packages/preview-server/src/utils/caniemail/get-css-property-names.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { allCssProperties } from './all-css-properties'; - -export const getCssPropertyNames = (title: string, keywords: string | null) => { - if (allCssProperties.includes(title.replace(' property', ''))) - return [title.replace(' property', '')]; - - if (title.split('&').length > 1) { - return title - .split(/\s*&\s*/) - .map((piece) => piece.trim()) - .filter((possiblePropertyName) => - allCssProperties.includes(possiblePropertyName), - ); - } - - if (title.split(',').length > 1) { - return title - .split(/\s*,\s*/) - .map((piece) => piece.trim()) - .filter((possiblePropertyName) => - allCssProperties.includes(possiblePropertyName), - ); - } - - if (keywords) { - return keywords - .split(/\s*,\s*/) - .filter((keyword) => allCssProperties.includes(keyword)); - } - - return []; -}; diff --git a/packages/preview-server/src/utils/caniemail/get-css-property-with-value.ts b/packages/preview-server/src/utils/caniemail/get-css-property-with-value.ts deleted file mode 100644 index f0fcf3a7fb..0000000000 --- a/packages/preview-server/src/utils/caniemail/get-css-property-with-value.ts +++ /dev/null @@ -1,14 +0,0 @@ -const propertyRegex = - /(?[a-z-]+)\s*:\s*(?[a-zA-Z\-0-9()+*/_ ]+)/; - -export const getCssPropertyWithValue = (title: string) => { - const match = propertyRegex.exec(title.trim()); - if (match) { - const [_full, propertyName, propertyValue] = match; - return { - name: propertyName!, - value: propertyValue!, - }; - } - return undefined; -}; diff --git a/packages/preview-server/src/utils/caniemail/get-css-unit.ts b/packages/preview-server/src/utils/caniemail/get-css-unit.ts deleted file mode 100644 index b88ca2c9da..0000000000 --- a/packages/preview-server/src/utils/caniemail/get-css-unit.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const getCssUnit = (title: string) => { - return title.endsWith(' unit') ? title.replace(' unit', '') : undefined; -}; diff --git a/packages/preview-server/src/utils/caniemail/get-element-attributes.ts b/packages/preview-server/src/utils/caniemail/get-element-attributes.ts deleted file mode 100644 index 5e79e3863d..0000000000 --- a/packages/preview-server/src/utils/caniemail/get-element-attributes.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function getElementAttributes(title: string) { - if (title.endsWith(' attribute')) { - return [title.replace(' attribute', '')]; - } - - return []; -} diff --git a/packages/preview-server/src/utils/caniemail/get-element-names.ts b/packages/preview-server/src/utils/caniemail/get-element-names.ts deleted file mode 100644 index 0dc9551b99..0000000000 --- a/packages/preview-server/src/utils/caniemail/get-element-names.ts +++ /dev/null @@ -1,26 +0,0 @@ -export const getElementNames = (title: string, keywords: string | null) => { - const match = /<(?[^>]*)> element/.exec(title); - if (match) { - const [_full, elementName] = match; - - if (elementName) { - return [elementName.toLowerCase()]; - } - } - - if (keywords !== null && keywords.length > 0) { - return keywords - .toLowerCase() - .split(/\s*,\s*/) - .map((piece) => piece.trim()); - } - - if (title.split(',').length > 1) { - return title - .toLowerCase() - .split(/\s*,\s*/) - .map((piece) => piece.trim()); - } - - return []; -}; diff --git a/packages/preview-server/src/utils/caniemail/tailwind/get-tailwind-config.spec.ts b/packages/preview-server/src/utils/tailwind/get-tailwind-config.spec.ts similarity index 100% rename from packages/preview-server/src/utils/caniemail/tailwind/get-tailwind-config.spec.ts rename to packages/preview-server/src/utils/tailwind/get-tailwind-config.spec.ts diff --git a/packages/preview-server/src/utils/caniemail/tailwind/get-tailwind-config.ts b/packages/preview-server/src/utils/tailwind/get-tailwind-config.ts similarity index 94% rename from packages/preview-server/src/utils/caniemail/tailwind/get-tailwind-config.ts rename to packages/preview-server/src/utils/tailwind/get-tailwind-config.ts index 35f5eb608c..81531fb6b8 100644 --- a/packages/preview-server/src/utils/caniemail/tailwind/get-tailwind-config.ts +++ b/packages/preview-server/src/utils/tailwind/get-tailwind-config.ts @@ -4,10 +4,10 @@ import traverse from '@babel/traverse'; import type { TailwindConfig } from '@react-email/tailwind'; import * as esbuild from 'esbuild'; import type { RawSourceMap } from 'source-map-js'; -import type { AST } from '../../../actions/email-validation/check-compatibility'; -import { convertStackWithSourceMap } from '../../convert-stack-with-sourcemap'; -import { isErr } from '../../result'; -import { runBundledCode } from '../../run-bundled-code'; +import type { AST } from '../../actions/email-validation/check-compatibility'; +import { convertStackWithSourceMap } from '../convert-stack-with-sourcemap'; +import { isErr } from '../result'; +import { runBundledCode } from '../run-bundled-code'; export const getTailwindConfig = async ( sourceCode: string, diff --git a/packages/preview-server/src/utils/caniemail/tailwind/get-tailwind-metadata.spec.ts b/packages/preview-server/src/utils/tailwind/get-tailwind-metadata.spec.ts similarity index 100% rename from packages/preview-server/src/utils/caniemail/tailwind/get-tailwind-metadata.spec.ts rename to packages/preview-server/src/utils/tailwind/get-tailwind-metadata.spec.ts diff --git a/packages/preview-server/src/utils/caniemail/tailwind/get-tailwind-metadata.ts b/packages/preview-server/src/utils/tailwind/get-tailwind-metadata.ts similarity index 92% rename from packages/preview-server/src/utils/caniemail/tailwind/get-tailwind-metadata.ts rename to packages/preview-server/src/utils/tailwind/get-tailwind-metadata.ts index 629cb9b628..71b0d9f0f6 100644 --- a/packages/preview-server/src/utils/caniemail/tailwind/get-tailwind-metadata.ts +++ b/packages/preview-server/src/utils/tailwind/get-tailwind-metadata.ts @@ -4,7 +4,7 @@ import { type TailwindConfig, type TailwindSetup, } from '@react-email/tailwind'; -import type { AST } from '../../../actions/email-validation/check-compatibility'; +import type { AST } from '../../actions/email-validation/check-compatibility'; import { getTailwindConfig } from './get-tailwind-config'; export const getTailwindMetadata = async ( diff --git a/packages/preview-server/src/utils/caniemail/tailwind/tests/dummy-email-template.tsx b/packages/preview-server/src/utils/tailwind/tests/dummy-email-template.tsx similarity index 100% rename from packages/preview-server/src/utils/caniemail/tailwind/tests/dummy-email-template.tsx rename to packages/preview-server/src/utils/tailwind/tests/dummy-email-template.tsx diff --git a/packages/preview-server/src/utils/caniemail/tailwind/tests/tailwind.config.ts b/packages/preview-server/src/utils/tailwind/tests/tailwind.config.ts similarity index 100% rename from packages/preview-server/src/utils/caniemail/tailwind/tests/tailwind.config.ts rename to packages/preview-server/src/utils/tailwind/tests/tailwind.config.ts diff --git a/packages/react-email/package.json b/packages/react-email/package.json index 941721b3a8..9c8eda9328 100644 --- a/packages/react-email/package.json +++ b/packages/react-email/package.json @@ -3,16 +3,35 @@ "version": "5.3.0-canary.2", "description": "A live preview of your emails right in your browser.", "bin": { - "email": "./dist/index.js" + "email": "./dist/cli/index.js" }, "type": "module", "scripts": { "build": "tsdown", "build:watch": "tsdown --watch src", + "caniemail:fetch": "tsx ./scripts/fill-caniemail-data.mts", "clean": "rm -rf dist", "test": "vitest run", "test:watch": "vitest" }, + "files": [ + "dist/**" + ], + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + } + }, "license": "MIT", "repository": { "type": "git", diff --git a/packages/react-email/scripts/fill-caniemail-data.ts b/packages/react-email/scripts/fill-caniemail-data.ts new file mode 100644 index 0000000000..314b73924b --- /dev/null +++ b/packages/react-email/scripts/fill-caniemail-data.ts @@ -0,0 +1,29 @@ +import { promises as fs } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +export const caniemailDataURL = + 'https://www.caniemail.com/api/data-ordered.json'; + +const responseFromCaniemail = await fetch(caniemailDataURL); +if (!responseFromCaniemail.ok) { + throw new Error( + `Could not get the data from Caniemail and there is no cached data under your temporary folder to fallback for. + +This could be happneing for the following reasons: +- You don't have internet connectivity +- Caniemail is down +- Caniemail changed from where to fetch their data from, which means we need to fix this. If this is the case, please open up an issue.`, + ); +} + +const response = await responseFromCaniemail.json(); + +await fs.writeFile( + path.resolve(import.meta.dirname, '../src/caniemail/caniemail-data.ts'), + `import type { SupportEntry } from "./support-entry-parsing.js"; + +export const nicenames = ${JSON.stringify(response.nicenames, null, 2)}; + +export const supportEntries: SupportEntry[] = ${JSON.stringify(response.data, null, 2)}`, +); diff --git a/packages/react-email/src/actions/email-validation/__snapshots__/check-images.spec.tsx.snap b/packages/react-email/src/actions/email-validation/__snapshots__/check-images.spec.tsx.snap deleted file mode 100644 index 0f682691d1..0000000000 --- a/packages/react-email/src/actions/email-validation/__snapshots__/check-images.spec.tsx.snap +++ /dev/null @@ -1,84 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`checkImages() 1`] = ` -[ - { - "checks": [ - { - "metadata": { - "alt": undefined, - }, - "passed": false, - "type": "accessibility", - }, - { - "passed": true, - "type": "syntax", - }, - { - "passed": true, - "type": "security", - }, - { - "metadata": { - "fetchStatusCode": 200, - }, - "passed": true, - "type": "fetch_attempt", - }, - { - "metadata": { - "byteCount": 26808, - }, - "passed": true, - "type": "image_size", - }, - ], - "codeLocation": { - "column": 3, - "line": 2, - }, - "source": "https://resend.com/static/brand/resend-icon-white.png", - "status": "warning", - }, - { - "checks": [ - { - "metadata": { - "alt": "codepen challenges", - }, - "passed": true, - "type": "accessibility", - }, - { - "passed": true, - "type": "syntax", - }, - { - "passed": true, - "type": "security", - }, - { - "metadata": { - "fetchStatusCode": 200, - }, - "passed": true, - "type": "fetch_attempt", - }, - { - "metadata": { - "byteCount": 111922, - }, - "passed": true, - "type": "image_size", - }, - ], - "codeLocation": { - "column": 3, - "line": 3, - }, - "source": "/static/codepen-challengers.png", - "status": "success", - }, -] -`; diff --git a/packages/preview-server/src/utils/caniemail/all-css-properties.ts b/packages/react-email/src/caniemail/all-css-properties.ts similarity index 100% rename from packages/preview-server/src/utils/caniemail/all-css-properties.ts rename to packages/react-email/src/caniemail/all-css-properties.ts diff --git a/packages/react-email/src/caniemail/data.ts b/packages/react-email/src/caniemail/data.ts new file mode 100644 index 0000000000..4a717e5203 --- /dev/null +++ b/packages/react-email/src/caniemail/data.ts @@ -0,0 +1,86441 @@ +import type { SupportEntry } from "./support-entry-parsing.js"; + +export const nicenames = { + "family": { + "gmail": "Gmail", + "outlook": "Outlook", + "yahoo": "Yahoo! Mail", + "apple-mail": "Apple Mail", + "aol": "AOL", + "thunderbird": "Mozilla Thunderbird", + "microsoft": "Microsoft", + "samsung-email": "Samsung Email", + "sfr": "SFR", + "orange": "Orange", + "protonmail": "ProtonMail", + "hey": "HEY", + "mail-ru": "Mail.ru", + "fastmail": "Fastmail", + "laposte": "LaPoste.net", + "t-online-de": "T-online.de", + "free-fr": "Free.fr", + "gmx": "GMX", + "web-de": "WEB.DE", + "ionos-1and1": "1&1", + "rainloop": "RainLoop", + "wp-pl": "WP.pl" + }, + "platform": { + "desktop-app": "Desktop", + "desktop-webmail": "Desktop Webmail", + "mobile-webmail": "Mobile Webmail", + "webmail": "Webmail", + "ios": "iOS", + "android": "Android", + "windows": "Windows", + "macos": "macOS", + "windows-mail": "Windows Mail", + "outlook-com": "Outlook.com" + }, + "support": { + "supported": "Supported", + "mitigated": "Partially supported", + "unsupported": "Not supported", + "unknown": "Support unknown", + "mixed": "Mixed support" + }, + "category": { + "html": "HTML", + "css": "CSS", + "image": "Image formats", + "others": "Others" + } +}; + +export const supportEntries: SupportEntry[] = [ + { + "slug": "amp", + "title": "AMP for Email", + "description": "Support for rendering emails in the AMP format.", + "url": "https://www.caniemail.com/features/amp/", + "category": "others", + "tags": [], + "keywords": "amp4email", + "last_test_date": "2020-03-31", + "test_url": "https://www.caniemail.com/tests/amp.html", + "test_results_url": null, + "stats": { + "apple-mail": { + "macos": [ + { + "12.4": "n" + } + ], + "ios": [ + { + "13.1": "n" + } + ] + }, + "gmail": { + "desktop-webmail": [ + { + "2020-04": "y" + }, + { + "2022-02": "y #1" + } + ], + "ios": [ + { + "2020-04": "y" + } + ], + "android": [ + { + "2020-04": "y" + } + ], + "mobile-webmail": [ + { + "2020-04": "n" + } + ] + }, + "orange": { + "desktop-webmail": [ + { + "2020-01": "n" + }, + { + "2021-03": "n" + } + ], + "ios": [ + { + "2020-01": "n" + } + ], + "android": [ + { + "2020-01": "n" + } + ] + }, + "outlook": { + "windows": [ + { + "2007": "n" + }, + { + "2010": "n" + }, + { + "2013": "n" + }, + { + "2016": "n" + }, + { + "2019": "n" + } + ], + "windows-mail": [ + { + "2019-10": "n" + } + ], + "macos": [ + { + "2019-10": "n" + }, + { + "16.80": "n" + } + ], + "outlook-com": [ + { + "2020-01": "n" + } + ], + "ios": [ + { + "2019-10": "n" + } + ], + "android": [ + { + "2019-10": "n" + } + ] + }, + "yahoo": { + "desktop-webmail": [ + { + "2019-10": "y" + }, + { + "2021-01": "y" + }, + { + "2022-02": "y #1" + } + ], + "ios": [ + { + "2019-10": "n" + }, + { + "2022-12": "y" + } + ], + "android": [ + { + "2019-10": "n" + }, + { + "2022-12": "y" + } + ] + }, + "aol": { + "desktop-webmail": [ + { + "2019-10": "n" + } + ], + "ios": [ + { + "2019-10": "n" + } + ], + "android": [ + { + "2019-10": "n" + } + ] + }, + "samsung-email": { + "android": [ + { + "5.0.10.2": "n" + } + ] + }, + "sfr": { + "desktop-webmail": [ + { + "2020-01": "n" + } + ], + "ios": [ + { + "2020-01": "n" + } + ], + "android": [ + { + "2020-01": "n" + } + ] + }, + "thunderbird": { + "macos": [ + { + "68.4": "n" + } + ] + }, + "protonmail": { + "desktop-webmail": [ + { + "2020-03": "n" + } + ], + "ios": [ + { + "2020-03": "n" + } + ], + "android": [ + { + "2020-03": "n" + } + ] + }, + "hey": { + "desktop-webmail": [ + { + "2020-06": "n" + } + ] + }, + "mail-ru": { + "desktop-webmail": [ + { + "2020-10": "y" + } + ] + }, + "fastmail": { + "desktop-webmail": [ + { + "2021-07": "n" + } + ] + }, + "laposte": { + "desktop-webmail": [ + { + "2021-08": "n" + } + ] + }, + "free-fr": { + "desktop-webmail": [ + { + "2022-12": "n" + } + ] + }, + "t-online-de": { + "desktop-webmail": [ + { + "2022-12": "n" + } + ] + }, + "gmx": { + "desktop-webmail": [ + { + "2022-06": "n" + } + ], + "ios": [ + { + "2022-06": "n" + } + ], + "android": [ + { + "2022-06": "n" + } + ] + }, + "web-de": { + "desktop-webmail": [ + { + "2022-06": "n" + } + ], + "ios": [ + { + "2022-06": "n" + } + ], + "android": [ + { + "2022-06": "n" + } + ] + }, + "ionos-1and1": { + "desktop-webmail": [ + { + "2022-06": "n" + } + ], + "android": [ + { + "2022-06": "n" + } + ] + } + }, + "notes": null, + "notes_by_num": { + "1": "Supported on compatible browsers. Refer to ‘supported platforms’ links listed below under resources." + } + }, + { + "slug": "bimi", + "title": "BIMI", + "description": "BIMI (Brand Indicators for Message Identification) is a specification allowing for the display of brand logos next to authenticated e-mails.", + "url": "https://www.caniemail.com/features/bimi/", + "category": "others", + "tags": [], + "keywords": "bimi, logo, brand", + "last_test_date": "2022-12-29", + "test_url": "https://www.caniemail.com", + "test_results_url": null, + "stats": { + "apple-mail": { + "macos": [ + { + "15": "n" + }, + { + "16": "y" + } + ], + "ios": [ + { + "15": "n" + }, + { + "16": "y" + } + ] + }, + "gmail": { + "desktop-webmail": [ + { + "2023-01": "y" + } + ], + "ios": [ + { + "2023-01": "y" + } + ], + "android": [ + { + "2023-01": "y" + } + ], + "mobile-webmail": [ + { + "2023-01": "n" + } + ] + }, + "orange": { + "desktop-webmail": [ + { + "2023-01": "n" + } + ], + "ios": [ + { + "2023-01": "n" + } + ], + "android": [ + { + "2023-01": "n" + } + ] + }, + "outlook": { + "windows": [ + { + "2007": "n" + }, + { + "2010": "n" + }, + { + "2013": "n" + }, + { + "2016": "n" + }, + { + "2019": "n" + } + ], + "windows-mail": [ + { + "2023-01": "n" + } + ], + "macos": [ + { + "16.56": "n" + } + ], + "outlook-com": [ + { + "2023-01": "n" + } + ], + "ios": [ + { + "2023-01": "n" + } + ], + "android": [ + { + "2023-01": "n" + } + ] + }, + "samsung-email": { + "android": [ + { + "6.0": "n" + } + ] + }, + "sfr": { + "desktop-webmail": [ + { + "2023-01": "n" + } + ], + "ios": [ + { + "2023-01": "n" + } + ], + "android": [ + { + "2023-01": "n" + } + ] + }, + "thunderbird": { + "macos": [ + { + "78.14": "n" + } + ] + }, + "aol": { + "desktop-webmail": [ + { + "2023-01": "n" + } + ], + "ios": [ + { + "2023-01": "n" + } + ], + "android": [ + { + "2023-01": "n" + } + ] + }, + "yahoo": { + "desktop-webmail": [ + { + "2023-01": "y" + } + ], + "ios": [ + { + "2023-01": "y" + } + ], + "android": [ + { + "2023-01": "y" + } + ] + }, + "protonmail": { + "desktop-webmail": [ + { + "2023-01": "n" + } + ], + "ios": [ + { + "2023-01": "n" + } + ], + "android": [ + { + "2023-01": "n" + } + ] + }, + "hey": { + "desktop-webmail": [ + { + "2023-01": "n" + } + ] + }, + "mail-ru": { + "desktop-webmail": [ + { + "2023-01": "n" + } + ] + }, + "fastmail": { + "desktop-webmail": [ + { + "2023-01": "y" + } + ] + }, + "laposte": { + "desktop-webmail": [ + { + "2022-08": "y" + } + ] + }, + "free-fr": { + "desktop-webmail": [ + { + "2023-01": "n" + } + ] + }, + "gmx": { + "desktop-webmail": [ + { + "2023-01": "n" + } + ] + }, + "t-online-de": { + "desktop-webmail": [ + { + "2023-01": "n" + } + ] + } + }, + "notes": "Data based on email clients providers own declarations.", + "notes_by_num": null + }, + { + "slug": "css-accent-color", + "title": "accent-color", + "description": "", + "url": "https://www.caniemail.com/features/css-accent-color/", + "category": "css", + "tags": [], + "keywords": "accent,color", + "last_test_date": "2023-12-19", + "test_url": "https://www.caniemail.com/tests/css-accent-color.html", + "test_results_url": "https://testi.at/proj/LAzSmlkimAnFmnrtPjPuPjpT1rO", + "stats": { + "apple-mail": { + "macos": [ + { + "16": "n" + }, + { + "17": "n" + }, + { + "18": "n" + }, + { + "19": "n" + }, + { + "20": "n" + }, + { + "21": "y" + } + ], + "ios": [ + { + "11": "n" + }, + { + "12": "n" + }, + { + "13": "n" + }, + { + "14": "y #1" + }, + { + "15": "y #1" + } + ] + }, + "gmail": { + "desktop-webmail": [ + { + "2022-07": "n" + } + ], + "ios": [ + { + "2022-07": "n" + } + ], + "android": [ + { + "2022-07": "n" + } + ], + "mobile-webmail": [ + { + "2022-07": "n" + } + ] + }, + "orange": { + "desktop-webmail": [ + { + "2022-07": "n" + } + ], + "ios": [ + { + "2022-07": "n" + } + ], + "android": [ + { + "2022-07": "n" + } + ] + }, + "outlook": { + "windows": [ + { + "2007": "n" + }, + { + "2010": "n" + }, + { + "2013": "n" + }, + { + "2016": "n" + }, + { + "2019": "n" + }, + { + "2021": "n" + } + ], + "windows-mail": [ + { + "2022-07": "n" + } + ], + "macos": [ + { + "2022-07": "n" + }, + { + "16.80": "n" + } + ], + "outlook-com": [ + { + "2022-07": "n" + } + ], + "ios": [ + { + "2022-07": "n" + } + ], + "android": [ + { + "2022-07": "n" + } + ] + }, + "yahoo": { + "desktop-webmail": [ + { + "2022-07": "n" + } + ], + "ios": [ + { + "2022-07": "n" + } + ], + "android": [ + { + "2022-07": "n" + } + ] + }, + "aol": { + "desktop-webmail": [ + { + "2022-07": "n" + } + ], + "ios": [ + { + "2022-07": "n" + } + ], + "android": [ + { + "2022-07": "n" + } + ] + }, + "samsung-email": { + "android": [ + { + "2022-07": "y #1" + } + ] + }, + "sfr": { + "desktop-webmail": [ + { + "2022-07": "y" + } + ], + "ios": [ + { + "2022-07": "n" + } + ], + "android": [ + { + "2022-07": "n" + } + ] + }, + "protonmail": { + "desktop-webmail": [ + { + "2022-07": "n" + } + ], + "ios": [ + { + "2022-07": "n" + } + ], + "android": [ + { + "2022-07": "n" + } + ] + }, + "hey": { + "desktop-webmail": [ + { + "2022-07": "y" + } + ] + }, + "mail-ru": { + "desktop-webmail": [ + { + "2022-07": "y #1" + } + ] + }, + "fastmail": { + "desktop-webmail": [ + { + "2022-07": "n" + } + ] + } + }, + "notes": null, + "notes_by_num": { + "1": "Supports `accent-color` but rendering depends on browser support." + } + }, + { + "slug": "css-align-items", + "title": "align-items", + "description": "", + "url": "https://www.caniemail.com/features/css-align-items/", + "category": "css", + "tags": [], + "keywords": "align,items,flexbox,grid", + "last_test_date": "2023-12-19", + "test_url": "https://www.caniemail.com/tests/css-align-items.html", + "test_results_url": "https://app.emailonacid.com/app/acidtest/FvYneb1dhiR4we6rAOf4AC02oFa6ksA0sTWxbEjgmt6Mg/list", + "stats": { + "apple-mail": { + "macos": [ + { + "11": "y" + }, + { + "12": "y" + }, + { + "13": "y" + } + ], + "ios": [ + { + "11": "y" + }, + { + "12": "y" + }, + { + "13": "y" + }, + { + "14": "y" + } + ] + }, + "gmail": { + "desktop-webmail": [ + { + "2020-12": "n" + } + ], + "ios": [ + { + "2020-12": "n" + } + ], + "android": [ + { + "2020-12": "n" + } + ], + "mobile-webmail": [ + { + "2020-12": "n" + } + ] + }, + "orange": { + "desktop-webmail": [ + { + "2021-02": "y" + }, + { + "2021-03": "n" + } + ], + "ios": [ + { + "2021-03": "y" + }, + { + "2024-04": "n" + } + ], + "android": [ + { + "2021-03": "y" + }, + { + "2024-04": "n" + } + ] + }, + "outlook": { + "windows": [ + { + "2007": "n" + }, + { + "2010": "n" + }, + { + "2013": "n" + }, + { + "2016": "n" + }, + { + "2019": "n" + } + ], + "windows-mail": [ + { + "2020-12": "n" + } + ], + "macos": [ + { + "2020-12": "y" + }, + { + "16.80": "y" + } + ], + "outlook-com": [ + { + "2020-12": "y" + } + ], + "ios": [ + { + "2020-12": "y" + } + ], + "android": [ + { + "4.2048.4": "y" + } + ] + }, + "yahoo": { + "desktop-webmail": [ + { + "2020-12": "n" + } + ], + "ios": [ + { + "2021-03": "n" + } + ], + "android": [ + { + "6.16.2.1519779": "n" + } + ] + }, + "aol": { + "desktop-webmail": [ + { + "2020-12": "n" + } + ], + "ios": [ + { + "2021-03": "n" + } + ], + "android": [ + { + "2021-03": "n" + } + ] + }, + "samsung-email": { + "android": [ + { + "6.1.31.2": "y" + } + ] + }, + "sfr": { + "desktop-webmail": [ + { + "2021-03": "y" + } + ], + "ios": [ + { + "2021-03": "y" + } + ], + "android": [ + { + "2021-03": "y" + } + ] + }, + "thunderbird": { + "macos": [ + { + "2020-12": "y" + } + ] + }, + "protonmail": { + "desktop-webmail": [ + { + "2021-03": "y" + } + ], + "ios": [ + { + "2021-03": "y" + } + ], + "android": [ + { + "2021-03": "y" + } + ] + }, + "hey": { + "desktop-webmail": [ + { + "2021-03": "y" + } + ] + }, + "mail-ru": { + "desktop-webmail": [ + { + "2020-12": "y" + } + ] + }, + "fastmail": { + "desktop-webmail": [ + { + "2021-07": "y" + } + ] + }, + "laposte": { + "desktop-webmail": [ + { + "2021-08": "y #1" + } + ] + }, + "gmx": { + "desktop-webmail": [ + { + "2022-06": "n" + } + ], + "ios": [ + { + "2022-06": "y" + } + ], + "android": [ + { + "2022-06": "y" + } + ] + }, + "web-de": { + "desktop-webmail": [ + { + "2022-06": "n" + } + ], + "ios": [ + { + "2022-06": "y" + } + ], + "android": [ + { + "2022-06": "y" + } + ] + }, + "ionos-1and1": { + "desktop-webmail": [ + { + "2022-06": "y" + } + ], + "android": [ + { + "2022-06": "y" + } + ] + } + }, + "notes": null, + "notes_by_num": { + "1": "Supported. But a default style of `margin:auto` is applied on every element and can prevent the expected result." + } + }, + { + "slug": "css-animation", + "title": "animation", + "description": "Tests for the shorthand `animation` property and its longhand equivalents.", + "url": "https://www.caniemail.com/features/css-animation/", + "category": "css", + "tags": [], + "keywords": "keyframes", + "last_test_date": "2023-12-19", + "test_url": "https://www.caniemail.com/tests/css-animation.html", + "test_results_url": "https://app.emailonacid.com/app/acidtest/u4oWccYOFNNyTagHs2NSUZqJYQ3MssrqDMocBnRa35hf7/list", + "stats": { + "apple-mail": { + "macos": [ + { + "13": "y" + } + ], + "ios": [ + { + "13": "y" + } + ] + }, + "gmail": { + "desktop-webmail": [ + { + "2021-05": "n" + } + ], + "ios": [ + { + "2021-05": "n" + } + ], + "android": [ + { + "2021-05": "n" + } + ], + "mobile-webmail": [ + { + "2021-05": "n" + } + ] + }, + "orange": { + "desktop-webmail": [ + { + "2021-05": "n" + } + ], + "ios": [ + { + "2021-05": "n" + } + ], + "android": [ + { + "2021-05": "n" + } + ] + }, + "outlook": { + "windows": [ + { + "2003": "n" + }, + { + "2007": "n" + }, + { + "2010": "n" + }, + { + "2013": "n" + }, + { + "2016": "n" + }, + { + "2019": "n" + } + ], + "windows-mail": [ + { + "2021-05": "n" + } + ], + "macos": [ + { + "2011": "y" + }, + { + "2016": "y" + }, + { + "16.80": "n" + } + ], + "outlook-com": [ + { + "2021-05": "n" + } + ], + "ios": [ + { + "2021-05": "n" + } + ], + "android": [ + { + "2021-05": "n" + } + ] + }, + "samsung-email": { + "android": [ + { + "6.1": "y" + } + ] + }, + "sfr": { + "desktop-webmail": [ + { + "2021-05": "a #1" + } + ], + "ios": [ + { + "2021-05": "n" + } + ], + "android": [ + { + "2021-05": "n" + } + ] + }, + "thunderbird": { + "macos": [ + { + "78.10": "y" + } + ] + }, + "aol": { + "desktop-webmail": [ + { + "2021-05": "n" + } + ], + "ios": [ + { + "2021-05": "n" + } + ], + "android": [ + { + "2021-05": "n" + } + ] + }, + "yahoo": { + "desktop-webmail": [ + { + "2021-05": "n" + } + ], + "ios": [ + { + "2021-05": "n" + } + ], + "android": [ + { + "2021-05": "n" + } + ] + }, + "protonmail": { + "desktop-webmail": [ + { + "2021-05": "n" + } + ], + "ios": [ + { + "2021-05": "n" + } + ], + "android": [ + { + "2021-05": "n" + } + ] + }, + "hey": { + "desktop-webmail": [ + { + "2021-05": "y" + } + ] + }, + "mail-ru": { + "desktop-webmail": [ + { + "2021-05": "n" + } + ] + }, + "fastmail": { + "desktop-webmail": [ + { + "2021-07": "y" + } + ] + }, + "laposte": { + "desktop-webmail": [ + { + "2021-08": "a #1" + } + ] + }, + "gmx": { + "desktop-webmail": [ + { + "2022-06": "n" + } + ], + "ios": [ + { + "2022-06": "y" + } + ], + "android": [ + { + "2022-06": "y" + } + ] + }, + "web-de": { + "desktop-webmail": [ + { + "2022-06": "n" + } + ], + "ios": [ + { + "2022-06": "y" + } + ], + "android": [ + { + "2022-06": "y" + } + ] + }, + "ionos-1and1": { + "desktop-webmail": [ + { + "2022-06": "a #2" + } + ], + "android": [ + { + "2022-06": "y" + } + ] + } + }, + "notes": null, + "notes_by_num": { + "1": "Buggy. Animation properties are supported but `@keyframes` are incorrectly prefixed.", + "2": "Partial. Only supports from and to keyframes. Does not support % keyframes" + } + }, + { + "slug": "css-aspect-ratio", + "title": "aspect-ratio", + "description": "Sets a preferred aspect ratio for the element", + "url": "https://www.caniemail.com/features/css-aspect-ratio/", + "category": "css", + "tags": [], + "keywords": "ratio", + "last_test_date": "2023-12-19", + "test_url": "https://www.caniemail.com/tests/css-aspect-ratio.html", + "test_results_url": "https://testi.at/proj/Mv0IO0vs3vTgRQuJ8IzyBfD6", + "stats": { + "apple-mail": { + "macos": [ + { + "14": "n" + }, + { + "15.0": "y" + } + ], + "ios": [ + { + "11": "n" + }, + { + "12": "n" + }, + { + "13": "n" + }, + { + "14": "n" + }, + { + "15": "y" + } + ] + }, + "gmail": { + "desktop-webmail": [ + { + "2021-10": "n" + } + ], + "ios": [ + { + "2021-10": "n" + } + ], + "android": [ + { + "2021-10": "n" + } + ], + "mobile-webmail": [ + { + "2021-10": "n" + } + ] + }, + "orange": { + "desktop-webmail": [ + { + "2021-10": "n" + } + ], + "ios": [ + { + "2021-10": "n" + } + ], + "android": [ + { + "2021-10": "n" + } + ] + }, + "outlook": { + "windows": [ + { + "2007": "n" + }, + { + "2010": "n" + }, + { + "2013": "n" + }, + { + "2016": "n" + }, + { + "2019": "n" + } + ], + "windows-mail": [ + { + "2021-10": "n" + } + ], + "macos": [ + { + "2021-10": "n" + }, + { + "16.80": "n" + } + ], + "outlook-com": [ + { + "2021-10": "n" + }, + { + "2023-12": "n" + } + ], + "ios": [ + { + "2021-10": "n" + } + ], + "android": [ + { + "2021-10": "n" + } + ] + }, + "yahoo": { + "desktop-webmail": [ + { + "2021-10": "n" + } + ], + "ios": [ + { + "2021-10": "n" + } + ], + "android": [ + { + "6.37": "n" + } + ] + }, + "aol": { + "desktop-webmail": [ + { + "2021-10": "n" + } + ], + "ios": [ + { + "2021-10": "n" + } + ], + "android": [ + { + "2021-10": "n" + } + ] + }, + "samsung-email": { + "android": [ + { + "2021-10": "n" + } + ] + }, + "sfr": { + "desktop-webmail": [ + { + "2021-11": "y" + } + ], + "ios": [ + { + "2021-11": "y #1" + } + ], + "android": [ + { + "2021-11": "y" + } + ] + }, + "thunderbird": { + "macos": [ + { + "78.10.2": "n" + } + ] + }, + "protonmail": { + "desktop-webmail": [ + { + "2021-11": "y" + } + ], + "ios": [ + { + "2021-11": "y #1" + } + ], + "android": [ + { + "2021-11": "y" + } + ] + }, + "hey": { + "desktop-webmail": [ + { + "2021-11": "y" + } + ] + }, + "mail-ru": { + "desktop-webmail": [ + { + "2021-10": "y" + } + ] + }, + "fastmail": { + "desktop-webmail": [ + { + "2021-11": "n" + } + ] + }, + "laposte": { + "desktop-webmail": [ + { + "2021-10": "y" + } + ] + }, + "gmx": { + "desktop-webmail": [ + { + "2022-06": "n" + } + ], + "ios": [ + { + "2022-06": "y" + } + ], + "android": [ + { + "2022-06": "y" + } + ] + }, + "web-de": { + "desktop-webmail": [ + { + "2022-06": "n" + } + ], + "ios": [ + { + "2022-06": "y" + } + ], + "android": [ + { + "2022-06": "y" + } + ] + }, + "ionos-1and1": { + "desktop-webmail": [ + { + "2022-06": "y" + } + ], + "android": [ + { + "2022-06": "y" + } + ] + } + }, + "notes": null, + "notes_by_num": { + "1": "Requires iOS 15." + } + }, + { + "slug": "css-at-font-face", + "title": "@font-face", + "description": "`@font-face` in CSS allows to include your own fonts inside an email.", + "url": "https://www.caniemail.com/features/css-at-font-face/", + "category": "css", + "tags": [], + "keywords": "font face, web fonts, google fonts", + "last_test_date": "2023-12-19", + "test_url": "https://www.caniemail.com/tests/css-font-face.html", + "test_results_url": "https://app.emailonacid.com/app/acidtest/veY9MhuhgFeF1ly5crrhTXawfLJSwxgpYi27OElI7iSoc/list", + "stats": { + "apple-mail": { + "macos": [ + { + "12.2": "y" + } + ], + "ios": [ + { + "10.3": "y" + }, + { + "12.3.1": "y" + } + ] + }, + "gmail": { + "desktop-webmail": [ + { + "2019-07": "n #6" + } + ], + "ios": [ + { + "2019-07": "n" + } + ], + "android": [ + { + "2019-07": "n" + } + ], + "mobile-webmail": [ + { + "2020-02": "n" + } + ] + }, + "orange": { + "desktop-webmail": [ + { + "2019-05": "a #2" + }, + { + "2021-03": "n #7" + }, + { + "2024-03": "n" + } + ], + "ios": [ + { + "2019-07": "y" + }, + { + "2024-03": "n" + } + ], + "android": [ + { + "2019-07": "a #1" + }, + { + "2024-04": "n" + } + ] + }, + "outlook": { + "windows": [ + { + "2003": "a #3" + }, + { + "2007": "a #4 #5" + }, + { + "2010": "a #4 #5" + }, + { + "2013": "a #4 #5" + }, + { + "2016": "a #4 #5" + }, + { + "2019": "a #4" + } + ], + "windows-mail": [ + { + "2020-01": "n" + } + ], + "macos": [ + { + "2011": "y" + }, + { + "2016": "y" + }, + { + "16.80": "n" + } + ], + "outlook-com": [ + { + "2019-07": "n" + }, + { + "2023-12": "n" + } + ], + "ios": [ + { + "2.51.1": "y" + }, + { + "3.29.0": "n" + } + ], + "android": [ + { + "2019-07": "n" + } + ] + }, + "samsung-email": { + "android": [ + { + "6.0": "y #8" + }, + { + "2021-11": "y #8" + } + ] + }, + "sfr": { + "desktop-webmail": [ + { + "2019-07": "a #2" + }, + { + "2025-07": "n" + } + ], + "ios": [ + { + "2019-07": "n" + } + ], + "android": [ + { + "2019-07": "n" + } + ] + }, + "thunderbird": { + "macos": [ + { + "60.7": "y" + }, + { + "78.5": "y" + } + ] + }, + "aol": { + "desktop-webmail": [ + { + "2020-01": "n" + } + ], + "ios": [ + { + "2020-01": "n" + } + ], + "android": [ + { + "2020-01": "n" + } + ] + }, + "yahoo": { + "desktop-webmail": [ + { + "2019-07": "n" + } + ], + "ios": [ + { + "2019-07": "n" + } + ], + "android": [ + { + "2019-07": "n" + } + ] + }, + "protonmail": { + "desktop-webmail": [ + { + "2020-03": "n" + } + ], + "ios": [ + { + "2020-03": "n" + } + ], + "android": [ + { + "2020-03": "y" + } + ] + }, + "hey": { + "desktop-webmail": [ + { + "2020-06": "y" + } + ] + }, + "mail-ru": { + "desktop-webmail": [ + { + "2020-10": "n" + } + ] + }, + "fastmail": { + "desktop-webmail": [ + { + "2021-07": "n" + } + ] + }, + "laposte": { + "desktop-webmail": [ + { + "2021-08": "a #2" + }, + { + "2025-07": "n" + } + ] + }, + "gmx": { + "desktop-webmail": [ + { + "2022-06": "n" + } + ], + "ios": [ + { + "2022-06": "y" + } + ], + "android": [ + { + "2022-06": "n" + } + ] + }, + "web-de": { + "desktop-webmail": [ + { + "2022-06": "n" + } + ], + "ios": [ + { + "2022-06": "y" + } + ], + "android": [ + { + "2022-06": "n" + } + ] + }, + "ionos-1and1": { + "desktop-webmail": [ + { + "2022-06": "y" + } + ], + "android": [ + { + "2022-06": "n" + } + ] + } + }, + "notes": null, + "notes_by_num": { + "1": "Partial. Only supported through a `` tag.", + "2": "Partial. Only supported directly through a `