diff --git a/packages/builder/src/components/automation/SetupPanel/AutomationSchemaLayout.svelte b/packages/builder/src/components/automation/SetupPanel/AutomationSchemaLayout.svelte index 762a416cf6..691d247881 100644 --- a/packages/builder/src/components/automation/SetupPanel/AutomationSchemaLayout.svelte +++ b/packages/builder/src/components/automation/SetupPanel/AutomationSchemaLayout.svelte @@ -100,6 +100,20 @@ comp: ExecuteScriptV2, fullWidth: true, }, + [SchemaFieldTypes.LONGFORM]: { + comp: DrawerBindableInput, + props: (opts: FieldProps = {} as FieldProps) => { + const { key, field } = opts + return { + title: field.title ?? getFieldLabel(key, field), + panel: AutomationBindingPanel, + type: field.customType, + updateOnChange: false, + multiline: true, + placeholder: field?.description, + } + }, + }, [SchemaFieldTypes.BOOL]: { comp: Checkbox, }, @@ -309,7 +323,7 @@ */ const getFieldType = ( field: BaseIOStructure, - block?: AutomationStep | AutomationTrigger + block: AutomationStep | AutomationTrigger ) => { // Direct customType map const customType = field.customType && customTypeToSchema[field.customType] diff --git a/packages/builder/src/components/automation/SetupPanel/BlockProperties.svelte b/packages/builder/src/components/automation/SetupPanel/BlockProperties.svelte index 6458c7aee8..3ddc854482 100644 --- a/packages/builder/src/components/automation/SetupPanel/BlockProperties.svelte +++ b/packages/builder/src/components/automation/SetupPanel/BlockProperties.svelte @@ -57,7 +57,7 @@ {#if customLayout} - + {:else} diff --git a/packages/builder/src/types/automations.ts b/packages/builder/src/types/automations.ts index fbaa962de1..9a7aa0f59d 100644 --- a/packages/builder/src/types/automations.ts +++ b/packages/builder/src/types/automations.ts @@ -24,6 +24,7 @@ export enum DataMode { } export enum SchemaFieldTypes { + LONGFORM = "longform", JSON = "json", ENUM = "enum", BOOL = "boolean", @@ -160,6 +161,7 @@ export const customTypeToSchema: Record = { export const typeToSchema: Partial> = { [AutomationIOType.BOOLEAN]: SchemaFieldTypes.BOOL, + [AutomationIOType.LONGFORM]: SchemaFieldTypes.LONGFORM, [AutomationIOType.DATE]: SchemaFieldTypes.DATE, [AutomationIOType.JSON]: SchemaFieldTypes.JSON, [AutomationIOType.ATTACHMENT]: SchemaFieldTypes.FILE, diff --git a/packages/shared-core/src/automations/steps/sendSmtpEmail.ts b/packages/shared-core/src/automations/steps/sendSmtpEmail.ts index 99ded7fdb3..1f352c6dc0 100644 --- a/packages/shared-core/src/automations/steps/sendSmtpEmail.ts +++ b/packages/shared-core/src/automations/steps/sendSmtpEmail.ts @@ -43,8 +43,10 @@ export const definition: AutomationStepDefinition = { title: "Email Subject", }, contents: { - type: AutomationIOType.STRING, - title: "HTML Contents", + type: AutomationIOType.LONGFORM, + title: "Message", + description: + "Use markdown or HTML for rich text formatting: **bold**, _italics_, # Headings, * Bullets,
for line breaks, and more.", }, addInvite: { type: AutomationIOType.BOOLEAN, diff --git a/packages/types/src/documents/app/automation/automation.ts b/packages/types/src/documents/app/automation/automation.ts index f74326ffe6..e9d05f3fcd 100644 --- a/packages/types/src/documents/app/automation/automation.ts +++ b/packages/types/src/documents/app/automation/automation.ts @@ -16,6 +16,7 @@ export enum AutomationIOType { DATE = "date", DATETIME = "datetime", ATTACHMENT = "attachment", + LONGFORM = "longform", } export enum AutomationCustomIOType { diff --git a/packages/worker/package.json b/packages/worker/package.json index 4ed98fd532..482544a0a8 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -54,6 +54,7 @@ "dotenv": "8.6.0", "email-validator": "^2.0.4", "global-agent": "3.0.0", + "http-graceful-shutdown": "^3.1.12", "ical-generator": "4.1.0", "joi": "17.6.0", "jsonwebtoken": "9.0.2", @@ -68,6 +69,7 @@ "koa-static": "5.0.0", "koa-useragent": "^4.1.0", "lodash": "4.17.21", + "marked": "^15.0.11", "node-fetch": "2.6.7", "nodemailer": "6.9.9", "passport-google-oauth": "2.0.0", @@ -75,8 +77,7 @@ "pouchdb": "7.3.0", "pouchdb-all-dbs": "1.1.1", "server-destroy": "1.0.1", - "uuid": "^8.3.2", - "http-graceful-shutdown": "^3.1.12" + "uuid": "^8.3.2" }, "devDependencies": { "@jest/types": "^29.6.3", @@ -92,6 +93,7 @@ "@types/server-destroy": "1.0.1", "@types/supertest": "2.0.14", "@types/uuid": "8.3.4", + "cheerio": "^1.0.0", "jest": "29.7.0", "maildev": "^2.2.1", "nock": "^13.5.4", diff --git a/packages/worker/src/api/routes/global/tests/email.spec.ts b/packages/worker/src/api/routes/global/tests/email.spec.ts index bbc6000b58..f4fd2d0297 100644 --- a/packages/worker/src/api/routes/global/tests/email.spec.ts +++ b/packages/worker/src/api/routes/global/tests/email.spec.ts @@ -9,6 +9,7 @@ import { stopMailserver, } from "../../../../tests/mocks/email" import { objectStore } from "@budibase/backend-core" +import * as cheerio from "cheerio" describe("/api/global/email", () => { const config = new TestConfiguration() @@ -266,4 +267,118 @@ describe("/api/global/email", () => { `DTEND:${formatDate(endTime)}` ) }) + + it("Should parse valid markdown content from automation steps into valid HTML.", async () => { + // Basic verification that the markdown is being processed. + const email = await captureEmail(mailserver, async () => { + const res = await config.api.emails.sendEmail({ + email: "to@example.com", + subject: "Test", + userId: config.user!._id, + contents: `test@home.com [Call Me!](tel:1111111)`, + purpose: EmailTemplatePurpose.CUSTOM, + }) + expect(res.message).toBeDefined() + }) + + const $ = cheerio.load(email.html) + + // Verify the email body rendered + const emailBody = $("td.email-body").first() + expect(emailBody.length).toBe(1) + + // Verify a valid link was generated and is queryable + const emailLink = $("a[href^='mailto:']").first() + expect(emailLink.length).toBe(1) + expect(emailLink.text()).toBe("test@home.com") + + // Verify the markdown link has been built correctly + const phoneLink = $("a[href^='tel:']").first() + expect(phoneLink.length).toBe(1) + expect(phoneLink.text()).toBe("Call Me!") + expect(phoneLink.attr("href")).toBe("tel:1111111") + }) + + it("Should ignore invalid markdown content and return nothing", async () => { + // The only failure case for a parse with marked is 'undefined' + // It should be caught and resolve to nothing. + const email = await captureEmail(mailserver, async () => { + const res = await config.api.emails.sendEmail({ + email: "to@example.com", + subject: "Test", + userId: config.user!._id, + contents: undefined, + purpose: EmailTemplatePurpose.CUSTOM, + }) + expect(res.message).toBeDefined() + }) + + const $ = cheerio.load(email.html) + const emailBody = $("td.email-body").first() + expect(emailBody.length).toBe(1) + + const bodyText = emailBody.text().trim() + expect(bodyText).toBe("") + }) + + it("Should render a mixture of content. Plain text, markdown and HTML", async () => { + // A more involved check to ensure all content types are still respected + const email = await captureEmail(mailserver, async () => { + const res = await config.api.emails.sendEmail({ + email: "to@example.com", + subject: "Test", + userId: config.user!._id, + contents: `
Some content
+ +# A heading + - This should be list entry 1 + - This should be list entry 2 + +Some plain text`, + purpose: EmailTemplatePurpose.CUSTOM, + }) + expect(res.message).toBeDefined() + }) + + const $ = cheerio.load(email.html) + const emailBody = $("td.email-body").first() + expect(emailBody.length).toBe(1) + + const divEle = emailBody.find("div.html-content").first() + expect(divEle.length).toBe(1) + expect(divEle.text()).toBe("Some content") + + const heading = emailBody.find("h1").first() + expect(heading.length).toBe(1) + expect(heading.text()).toBe("A heading") + + // Both list items rendered + const listEles = emailBody.find("ul li") + expect(listEles.length).toBe(2) + + const plainText = emailBody.find("p") + expect(plainText.length).toBe(1) + expect(plainText.text()).toBe("Some plain text") + }) + + it("Should only parse markdown content for the CUSTOM email template used in automation steps", async () => { + const email = await captureEmail(mailserver, async () => { + const res = await config.api.emails.sendEmail({ + email: "to@example.com", + subject: "Test", + userId: config.user!._id, + purpose: EmailTemplatePurpose.INVITATION, + }) + expect(res.message).toBeDefined() + }) + + const $ = cheerio.load(email.html) + const emailBody = $("td.email-body").first() + expect(emailBody.length).toBe(1) + + const heading = emailBody.find("h1").first() + expect(heading.length).toBe(1) + // The email should not be parsed as markdown. + expect(heading.text()).toBe("Hi, to@example.com!") + }) }) diff --git a/packages/worker/src/utilities/email.ts b/packages/worker/src/utilities/email.ts index 411f6b1e4a..2b144573bb 100644 --- a/packages/worker/src/utilities/email.ts +++ b/packages/worker/src/utilities/email.ts @@ -12,6 +12,7 @@ import { import { configs, cache, objectStore, HTTPError } from "@budibase/backend-core" import ical from "ical-generator" import _ from "lodash" +import { marked } from "marked" import nodemailer from "nodemailer" import SMTPTransport from "nodemailer/lib/smtp-transport" @@ -97,7 +98,10 @@ async function buildEmail( } context = { ...context, - contents, + contents: + purpose === EmailTemplatePurpose.CUSTOM + ? marked.parse(contents || "") + : contents, email, name, user: user || {}, diff --git a/yarn.lock b/yarn.lock index d14128e14a..dbc98bd996 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8539,6 +8539,11 @@ body-parser@1.20.3: type-is "~1.6.18" unpipe "1.0.0" +boolbase@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== + boolean@^3.0.1: version "3.2.0" resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.2.0.tgz#9e5294af4e98314494cbb17979fa54ca159f116b" @@ -8999,6 +9004,35 @@ check-error@^1.0.3: dependencies: get-func-name "^2.0.2" +cheerio-select@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-2.1.0.tgz#4d8673286b8126ca2a8e42740d5e3c4884ae21b4" + integrity sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g== + dependencies: + boolbase "^1.0.0" + css-select "^5.1.0" + css-what "^6.1.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + domutils "^3.0.1" + +cheerio@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0.tgz#1ede4895a82f26e8af71009f961a9b8cb60d6a81" + integrity sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww== + dependencies: + cheerio-select "^2.1.0" + dom-serializer "^2.0.0" + domhandler "^5.0.3" + domutils "^3.1.0" + encoding-sniffer "^0.2.0" + htmlparser2 "^9.1.0" + parse5 "^7.1.2" + parse5-htmlparser2-tree-adapter "^7.0.0" + parse5-parser-stream "^7.1.2" + undici "^6.19.5" + whatwg-mimetype "^4.0.0" + chokidar@3.5.3, chokidar@^3.5.2, chokidar@^3.5.3: version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" @@ -9766,6 +9800,17 @@ css-line-break@^2.1.0: dependencies: utrie "^1.0.2" +css-select@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" + integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== + dependencies: + boolbase "^1.0.0" + css-what "^6.1.0" + domhandler "^5.0.2" + domutils "^3.0.1" + nth-check "^2.0.1" + css-tree@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.3.1.tgz#10264ce1e5442e8572fc82fbe490644ff54b5c20" @@ -9774,6 +9819,11 @@ css-tree@^2.3.1: mdn-data "2.0.30" source-map-js "^1.0.1" +css-what@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== + css.escape@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" @@ -10669,7 +10719,7 @@ domexception@^4.0.0: dependencies: webidl-conversions "^7.0.0" -domhandler@^5.0.1, domhandler@^5.0.2: +domhandler@^5.0.1, domhandler@^5.0.2, domhandler@^5.0.3: version "5.0.3" resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== @@ -10692,6 +10742,15 @@ domutils@^3.0.1: domelementtype "^2.3.0" domhandler "^5.0.1" +domutils@^3.1.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.2.2.tgz#edbfe2b668b0c1d97c24baf0f1062b132221bc78" + integrity sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw== + dependencies: + dom-serializer "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + dot-prop@^5.1.0, dot-prop@^5.2.0: version "5.3.0" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" @@ -10884,6 +10943,14 @@ encoding-down@^6.3.0: level-codec "^9.0.0" level-errors "^2.0.0" +encoding-sniffer@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz#799569d66d443babe82af18c9f403498365ef1d5" + integrity sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg== + dependencies: + iconv-lite "^0.6.3" + whatwg-encoding "^3.1.1" + encoding@^0.1.13: version "0.1.13" resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" @@ -10957,6 +11024,11 @@ entities@^4.2.0, entities@^4.3.0, entities@^4.4.0, entities@^4.5.0: resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== +entities@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-6.0.0.tgz#09c9e29cb79b0a6459a9b9db9efb418ac5bb8e51" + integrity sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw== + env-paths@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" @@ -13116,6 +13188,16 @@ htmlparser2@^8.0.0: domutils "^3.0.1" entities "^4.3.0" +htmlparser2@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-9.1.0.tgz#cdb498d8a75a51f739b61d3f718136c369bc8c23" + integrity sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.3" + domutils "^3.1.0" + entities "^4.5.0" + http-assert@^1.3.0: version "1.5.0" resolved "https://registry.yarnpkg.com/http-assert/-/http-assert-1.5.0.tgz#c389ccd87ac16ed2dfa6246fd73b926aa00e6b8f" @@ -15971,6 +16053,11 @@ map-obj@^4.0.0: resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a" integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ== +marked@^15.0.11: + version "15.0.11" + resolved "https://registry.yarnpkg.com/marked/-/marked-15.0.11.tgz#08a8d12c285e16259e44287b89ce0d871c9d55e8" + integrity sha512-1BEXAU2euRCG3xwgLVT1y0xbJEld1XOrmRJpUwRCcy7rxhSCwMrmEu9LXoPhHSCJG41V7YcQ2mjKRr5BA3ITIA== + marked@^15.0.8: version "15.0.8" resolved "https://registry.yarnpkg.com/marked/-/marked-15.0.8.tgz#39873a3fdf91a520111e48aeb2ef3746d58d7166" @@ -16942,6 +17029,13 @@ npmlog@^6.0.0, npmlog@^6.0.2: gauge "^4.0.3" set-blocking "^2.0.0" +nth-check@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== + dependencies: + boolbase "^1.0.0" + nunjucks@^3.2.3: version "3.2.4" resolved "https://registry.yarnpkg.com/nunjucks/-/nunjucks-3.2.4.tgz#f0878eef528ce7b0aa35d67cc6898635fd74649e" @@ -17607,6 +17701,21 @@ parse-url@^8.1.0: dependencies: parse-path "^7.0.0" +parse5-htmlparser2-tree-adapter@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz#b5a806548ed893a43e24ccb42fbb78069311e81b" + integrity sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g== + dependencies: + domhandler "^5.0.3" + parse5 "^7.0.0" + +parse5-parser-stream@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz#d7c20eadc37968d272e2c02660fff92dd27e60e1" + integrity sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow== + dependencies: + parse5 "^7.0.0" + parse5@6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" @@ -17617,6 +17726,13 @@ parse5@^1.5.1: resolved "https://registry.yarnpkg.com/parse5/-/parse5-1.5.1.tgz#9b7f3b0de32be78dc2401b17573ccaf0f6f59d94" integrity sha512-w2jx/0tJzvgKwZa58sj2vAYq/S/K1QJfIB3cWYea/Iu1scFPDQQ3IQiVZTHWtRBwAjv2Yd7S/xeZf3XqLDb3bA== +parse5@^7.0.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.3.0.tgz#d7e224fa72399c7a175099f45fc2ad024b05ec05" + integrity sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw== + dependencies: + entities "^6.0.0" + parse5@^7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" @@ -21600,6 +21716,11 @@ undici@^5.28.4: dependencies: "@fastify/busboy" "^2.0.0" +undici@^6.19.5: + version "6.21.2" + resolved "https://registry.yarnpkg.com/undici/-/undici-6.21.2.tgz#49c5884e8f9039c65a89ee9018ef3c8e2f1f4928" + integrity sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g== + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc"