AutomationFlowsEmail & Gmail › Validate Email Hero Images with Gmail, Dropbox, Ocr.space and Google Sheets

Validate Email Hero Images with Gmail, Dropbox, Ocr.space and Google Sheets

BySasikala Jayamani @sasikalajayamani on n8n.io

Expected Image Download The expected image’s Dropbox URL is passed directly into an HTTP Request node, which downloads the image as binary data.

Event trigger★★★★☆ complexity12 nodesGmailHTTP RequestGoogle Sheets
Email & Gmail Trigger: Event Nodes: 12 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #13679 — we link there as the canonical source.

This workflow follows the Gmail → Google Sheets recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "id": "jnJH1poep1kXrG2U",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "My workflow",
  "tags": [],
  "nodes": [
    {
      "id": "39d8d355-9c4a-4202-9ca9-0c3219d4e207",
      "name": "When clicking \u2018Execute workflow\u2019",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        -1168,
        1376
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "1933da34-7848-4659-b0c9-0ea81029e472",
      "name": "Get many messages",
      "type": "n8n-nodes-base.gmail",
      "position": [
        -912,
        1376
      ],
      "parameters": {
        "simple": false,
        "filters": {
          "sender": "user@example.com",
          "readStatus": "unread",
          "includeSpamTrash": true
        },
        "options": {},
        "operation": "getAll"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "694fb2d1-c31a-4b07-b320-434f7dfe8c3a",
      "name": "Extract Hero Image SRC From HTML",
      "type": "n8n-nodes-base.code",
      "position": [
        -544,
        1376
      ],
      "parameters": {
        "jsCode": "/**\n * n8n Function node\n * Extract hero <img> src, alt, and title from Gmail HTML.\n *\n * Input: items[0].json.html (string)\n * Output: { hero: { src, alt, title }, validations: {...}, raw: { imgTag, container } }\n */\n\nfunction extractAttr(tag, attrName) {\n  // Extracts attribute values from a tag string safely\n  const re = new RegExp(`${attrName}\\\\s*=\\\\s*(\"([^\"]*)\"|'([^']*)'|([^\\\\s>]+))`, 'i');\n  const m = tag.match(re);\n  return m ? (m[2] || m[3] || m[4] || '') : '';\n}\n\nfunction firstImgTag(html) {\n  // Find the hero container and the first <img> inside it\n\n  // 1) Prefer a container with id=\"heroImage...\"\n  const heroDivMatch = html.match(\n    /<div\\s+class=[\"']mktoImg[\"']\\s+id=[\"']heroImage[^\"']*[\"'][\\s\\S]*?>[\\s\\S]*?<\\/div>/i\n  );\n  if (heroDivMatch) {\n    const heroDiv = heroDivMatch[0];\n    const imgMatch = heroDiv.match(/<img\\b[^>]*>/i);\n    return { imgTag: imgMatch ? imgMatch[0] : '', container: heroDiv };\n  }\n\n  // 2) Fallback: module row with id starting \"fullWidthImageModule\"\n  const moduleMatch = html.match(\n    /<tr\\s+class=[\"']mktoModule[\"']\\s+id=[\"']fullWidthImageModule[^\"']*[\"'][\\s\\S]*?>[\\s\\S]*?<\\/tr>/i\n  );\n  if (moduleMatch) {\n    const mod = moduleMatch[0];\n    const imgMatch = mod.match(/<img\\b[^>]*>/i);\n    return { imgTag: imgMatch ? imgMatch[0] : '', container: mod };\n  }\n\n  // 3) Last resort: first centered hero-like <img> with max-width ~ 680\n  const genericMatch = html.match(\n    /<img\\b[^>]*\\b(max-width:\\s*680px|width\\s*=\\s*[\"']680[\"'])[^>]*>/i\n  );\n  return { imgTag: genericMatch ? genericMatch[0] : '', container: '' };\n}\n\nconst itemsOut = [];\n\nfor (const item of items) {\n  const html = (item.json && item.json.html) ? item.json.html : '';\n\n  if (!html || typeof html !== 'string') {\n    itemsOut.push({\n      json: {\n        error: 'HTML content not found at items[0].json.html',\n      },\n    });\n    continue;\n  }\n\n  const { imgTag, container } = firstImgTag(html);\n\n  if (!imgTag) {\n    itemsOut.push({\n      json: {\n        error: 'Hero image tag not found',\n        hint: 'Ensure the hero block has id=\"heroImage...\" or the module id starts with \"fullWidthImageModule\".',\n      },\n    });\n    continue;\n  }\n\n  // Extract attributes\n  const src = extractAttr(imgTag, 'src');\n  const alt = extractAttr(imgTag, 'alt');\n  const title = extractAttr(imgTag, 'title');\n\n  // Minimal validations\n  const validations = {\n    hasSrc: Boolean(src),\n    srcIsHttps: /^https:\\/\\//i.test(src),\n    altPresent: alt.trim().length > 0,\n    titlePresent: title.trim().length > 0,\n    altTitleSame: alt.trim() && title.trim() ? alt.trim() === title.trim() : null,\n  };\n\n  itemsOut.push({\n    json: {\n      hero: { src, alt, title },\n      validations,\n      raw: { imgTag, container },\n    },\n  });\n}\n\nreturn itemsOut;"
      },
      "typeVersion": 2
    },
    {
      "id": "022156ac-a677-4e25-96d4-fc4a5433bb20",
      "name": "Convert Image File from HTML to Binary",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -224,
        1504
      ],
      "parameters": {
        "url": "={\n  \"hero\": {\n    \"src\": \"https://dl.dropboxusercontent.com/scl/fi/<FILE_ID>/<generic-image.png>\",\n    \"alt\": \"Hero Image\",\n    \"title\": \"Hero Image Title\"\n  }\n}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "3a6f5dc9-0528-41c3-829b-9f8fb6414b96",
      "name": "Convert Image File from Dropbox to Binary",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -240,
        1312
      ],
      "parameters": {
        "url": "=https://dl.dropboxusercontent.com/scl/fi/<FILE_ID>/<generic-image.png>",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "7b721510-5fb1-4fc4-b9ee-16040e783f06",
      "name": "Combine Image Outputs",
      "type": "n8n-nodes-base.merge",
      "position": [
        256,
        1408
      ],
      "parameters": {},
      "typeVersion": 3.2
    },
    {
      "id": "4879f75c-c085-406b-9465-8e1dcacc408a",
      "name": "Extract Content Match Results",
      "type": "n8n-nodes-base.code",
      "position": [
        464,
        1408
      ],
      "parameters": {
        "jsCode": "// Get all input items\nconst items = $input.all();\n\n// Extract ParsedText from input1 and input2\nconst sourceAText = items[0]?.json?.ParsedResults?.[0]?.ParsedText || \"\";\nconst sourceBText = items[1]?.json?.ParsedResults?.[0]?.ParsedText || \"\";\n\n// Normalize strings\nfunction normalize(str) {\n  return str.replace(/\\s+/g, \" \").trim().toLowerCase();\n}\n\nconst normA = normalize(sourceAText);\nconst normB = normalize(sourceBText);\n\n// Comparison logic\nlet result;\nif (!normA && !normB) {\n  result = \"No content in either source\";\n} else if (!normA || !normB) {\n  result = \"Missing content in one source\";\n} else if (normA === normB) {\n  result = \"Exact Match\";\n} else if (normA.includes(normB) || normB.includes(normA)) {\n  result = \"Partial Match\";\n} else {\n  result = \"Mismatch\";\n}\n\n// Return as columns\nreturn [\n  {\n    \"Column A (Source A Content)\": sourceAText,\n    \"Column B (Source B Content)\": sourceBText,\n    \"Column C (Result)\": result\n  }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "1f58fa91-7a48-40ef-8ece-477878d496b2",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2176,
        1120
      ],
      "parameters": {
        "width": 832,
        "height": 624,
        "content": "## Main Overview \u2013 Image Content Validation Flow\n\nThis workflow automates the image content validation process for email QA by comparing the text extracted from expected images stored in Dropbox with the text extracted from actual images found in the email HTML. Instead of manually downloading images, running OCR tools, and checking if the promotional/header image content matches design specifications, this workflow performs the entire process automatically. The flow ensures consistency between the creative assets and the final email build, detects missing or incorrect image content, and logs standardized results back into Google Sheets.\n\n**How it works**\n1. The expected image Dropbox URL is passed directly into an HTTP node, which downloads the expected image in 2. binary format.\n3. The workflow extracts the actual image src from the email HTML (e.g., hero image or any targeted image block).\n4. The actual image is also downloaded in binary format using another HTTP node.\n5. Both binary images are sent individually to the OCR.Space API to extract readable text.\n6. A JS node compares the Expected Image Text vs Actual Image Text to determine a Match or Mismatch.\n7. The workflow logs Expected Text, Actual Text, and Result back into Google Sheets.\n\n**Setup steps**\n* Create Google Sheets columns:\n* SectionId, ExpectedImageURL, ExpectedText, ActualText, Result.\n* Ensure the Dropbox link is a direct-access URL (no login required).\n* Add your OCR.Space API key to the HTTP POST node.\n* Confirm the HTML node extracts the correct image src from the email HTML.\n* Test using a sample email to verify OCR accuracy and layout."
      },
      "typeVersion": 1
    },
    {
      "id": "33031145-aab5-42e7-8b88-a845d7ca5848",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -624,
        1200
      ],
      "parameters": {
        "color": 7,
        "width": 1552,
        "height": 512,
        "content": "## Image Validation\nFetch expected image from Dropbox and actual image from HTML, extract text using OCR.Space, compare expected vs actual text, and log extracted text + results into Google Sheet."
      },
      "typeVersion": 1
    },
    {
      "id": "78dbbd3e-ab76-4832-89b5-6169ac0a6577",
      "name": "Log Image Checks to Excel",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        704,
        1408
      ],
      "parameters": {
        "columns": {
          "value": {
            "Result": "={{ $json['Column C (Result)'] }}",
            "HTML Hero Content": "={{ $json['Column B (HTML Content)'] }}",
            "Dropbox Hero Content": "={{ $json['Column A (Dropbox Content)'] }}"
          },
          "schema": [
            {
              "id": "Dropbox Hero Content",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Dropbox Hero Content",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "HTML Hero Content",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "HTML Hero Content",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Result",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Result",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "Dropbox Hero Content"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "appendOrUpdate",
        "sheetName": {
          "__rl": true,
          "mode": "id",
          "value": "=// Parameters provided by user in node config\nconst sheetId = this.getNodeParameter(\"sheetId\", 0) || \"YOUR_SPREADSHEET_ID\";\nconst sheetName = this.getNodeParameter(\"sheetName\", 0) || \"Sheet1\";\n\nconst { google } = require(\"googleapis\");\nconst sheets = google.sheets({ version: \"v4\", auth });\n\nconst response = await sheets.spreadsheets.values.get({\n  spreadsheetId: sheetId,\n  range: sheetName,\n});\n\nreturn response.data.values.map(row => ({ json: { row } }));"
        },
        "documentId": {
          "__rl": true,
          "mode": "url",
          "value": "=https://docs.google.com/spreadsheets/d/<YOUR_SHEET_ID>/edit"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "48a348f7-2988-4dfe-84ff-638216262486",
      "name": "Extracting Expected Image Content from OCR1",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        16,
        1312
      ],
      "parameters": {
        "url": "=const response = await fetch(\"https://api.ocr.space/parse/image\", {\n  method: \"POST\",\n  headers: {\n    \"apikey\": process.env.OCR_API_KEY,   // use environment variable\n  },\n  body: new FormData({\n    file: myFile,                        // user-provided file\n    language: \"eng\",                     // configurable language\n  })\n});",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "contentType": "multipart-form-data",
        "sendHeaders": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "data",
              "parameterType": "formBinaryData",
              "inputDataFieldName": "data"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "=headers: {\n  \"apikey\": process.env.OCR_API_KEY\n}",
              "value": "=headers: {\n  \"apikey\": process.env.OCR_API_KEY\n}"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "69f800ff-39b6-4158-a0e7-8cfa966f208e",
      "name": "Extracting Actual Image Content from OCR",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        32,
        1488
      ],
      "parameters": {
        "url": "=const response = await fetch(\"https://api.ocr.space/parse/image\", {\n  method: \"POST\",\n  headers: {\n    \"apikey\": process.env.OCR_API_KEY,   // use environment variable\n  },\n  body: new FormData({\n    file: myFile,                        // user-provided file\n    language: \"eng\",                     // configurable language\n  })\n});",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "contentType": "multipart-form-data",
        "sendHeaders": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "data",
              "parameterType": "formBinaryData",
              "inputDataFieldName": "=data"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "=headers: {\n  \"apikey\": process.env.OCR_API_KEY\n}",
              "value": "=headers: {\n  \"apikey\": process.env.OCR_API_KEY\n}"
            }
          ]
        }
      },
      "typeVersion": 4.3
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "callerPolicy": "workflowsFromSameOwner",
    "timeSavedMode": "fixed",
    "availableInMCP": false,
    "executionOrder": "v1",
    "saveExecutionProgress": true
  },
  "versionId": "a06236d6-4ce7-4099-8233-697e253c4433",
  "connections": {
    "Get many messages": {
      "main": [
        [
          {
            "node": "Extract Hero Image SRC From HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Combine Image Outputs": {
      "main": [
        [
          {
            "node": "Extract Content Match Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log Image Checks to Excel": {
      "main": [
        []
      ]
    },
    "Extract Content Match Results": {
      "main": [
        [
          {
            "node": "Log Image Checks to Excel",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Hero Image SRC From HTML": {
      "main": [
        [
          {
            "node": "Convert Image File from HTML to Binary",
            "type": "main",
            "index": 0
          },
          {
            "node": "Convert Image File from Dropbox to Binary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When clicking \u2018Execute workflow\u2019": {
      "main": [
        [
          {
            "node": "Get many messages",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Convert Image File from HTML to Binary": {
      "main": [
        [
          {
            "node": "Extracting Actual Image Content from OCR",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extracting Actual Image Content from OCR": {
      "main": [
        [
          {
            "node": "Combine Image Outputs",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Convert Image File from Dropbox to Binary": {
      "main": [
        [
          {
            "node": "Extracting Expected Image Content from OCR1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extracting Expected Image Content from OCR1": {
      "main": [
        [
          {
            "node": "Combine Image Outputs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

Expected Image Download The expected image’s Dropbox URL is passed directly into an HTTP Request node, which downloads the image as binary data.

Source: https://n8n.io/workflows/13679/ — original creator credit. Request a take-down →

More Email & Gmail workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Email & Gmail

📘 Description

HTTP Request, Gmail Trigger, ClickUp +3
Email & Gmail

How it works

Gmail, HTTP Request, Google Sheets
Email & Gmail

Automatically extract structured information from emails using AI-powered document analysis. This workflow processes emails from specified domains, classifies them by type, and extracts structured dat

Gmail, HTTP Request, AWS S3 +1
Email & Gmail

Fetches all open sprint tickets daily from your Jira project Analyzes each ticket for overdue days and blocked status Routes to the right escalation level: assignee email → team Google Chat alert → ma

Gmail, Jira, HTTP Request
Email & Gmail

What This Flow Does

Gmail, Google Sheets, HTTP Request +1