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 →
{
"id": "gpsImapIngest0001",
"name": "GPS: Ingesta IMAP \u2192 gps_event",
"nodes": [
{
"parameters": {
"format": "resolved",
"postProcessAction": "read",
"options": {}
},
"id": "ImapTrigger",
"name": "Email Trigger (IMAP)",
"type": "n8n-nodes-base.emailReadImap",
"typeVersion": 2,
"position": [
-720,
0
],
"credentials": {
"imap": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// --- SHA-256 en JS puro (equivalente a crypto.createHash('sha256')) ---\n// Generado por scripts/build_ingest_workflow.js \u2014 no editar a mano.\nconst crypto = (() => {\n const K = [\n 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1,\n 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,\n 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,\n 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,\n 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147,\n 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,\n 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,\n 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,\n 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,\n 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,\n 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,\n ];\n\n const rotr = (x, n) => (x >>> n) | (x << (32 - n));\n\n function sha256Hex(input) {\n // El hash se calcula sobre los bytes UTF-8, igual que\n // crypto.createHash('sha256').update(s, 'utf8').\n const bytes = Array.from(new TextEncoder().encode(input));\n\n const bitLen = bytes.length * 8;\n bytes.push(0x80);\n while (bytes.length % 64 !== 56) bytes.push(0);\n\n // Longitud como entero de 64 bits big-endian.\n const hi = Math.floor(bitLen / 0x100000000);\n const lo = bitLen >>> 0;\n bytes.push((hi >>> 24) & 0xff, (hi >>> 16) & 0xff, (hi >>> 8) & 0xff, hi & 0xff);\n bytes.push((lo >>> 24) & 0xff, (lo >>> 16) & 0xff, (lo >>> 8) & 0xff, lo & 0xff);\n\n let h0 = 0x6a09e667, h1 = 0xbb67ae85, h2 = 0x3c6ef372, h3 = 0xa54ff53a;\n let h4 = 0x510e527f, h5 = 0x9b05688c, h6 = 0x1f83d9ab, h7 = 0x5be0cd19;\n\n const w = new Uint32Array(64);\n\n for (let i = 0; i < bytes.length; i += 64) {\n for (let t = 0; t < 16; t++) {\n w[t] =\n (bytes[i + t * 4] << 24) |\n (bytes[i + t * 4 + 1] << 16) |\n (bytes[i + t * 4 + 2] << 8) |\n bytes[i + t * 4 + 3];\n }\n\n for (let t = 16; t < 64; t++) {\n const s0 = rotr(w[t - 15], 7) ^ rotr(w[t - 15], 18) ^ (w[t - 15] >>> 3);\n const s1 = rotr(w[t - 2], 17) ^ rotr(w[t - 2], 19) ^ (w[t - 2] >>> 10);\n w[t] = (w[t - 16] + s0 + w[t - 7] + s1) >>> 0;\n }\n\n let a = h0, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, h = h7;\n\n for (let t = 0; t < 64; t++) {\n const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);\n const ch = (e & f) ^ (~e & g);\n const temp1 = (h + S1 + ch + K[t] + w[t]) >>> 0;\n const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);\n const maj = (a & b) ^ (a & c) ^ (b & c);\n const temp2 = (S0 + maj) >>> 0;\n\n h = g; g = f; f = e;\n e = (d + temp1) >>> 0;\n d = c; c = b; b = a;\n a = (temp1 + temp2) >>> 0;\n }\n\n h0 = (h0 + a) >>> 0; h1 = (h1 + b) >>> 0;\n h2 = (h2 + c) >>> 0; h3 = (h3 + d) >>> 0;\n h4 = (h4 + e) >>> 0; h5 = (h5 + f) >>> 0;\n h6 = (h6 + g) >>> 0; h7 = (h7 + h) >>> 0;\n }\n\n return [h0, h1, h2, h3, h4, h5, h6, h7]\n .map((x) => x.toString(16).padStart(8, '0'))\n .join('');\n }\n\n // Imita la interfaz encadenable que usa el parser.\n return {\n createHash() {\n let data = '';\n const api = {\n update(chunk) { data += chunk; return api; },\n digest() { return sha256Hex(data); },\n };\n return api;\n },\n };\n})();\n\nfunction stripHtml(html) {\n if (!html) return '';\n return String(html)\n .replace(/<br\\s*\\/?>/gi, '\\n')\n .replace(/<\\/p>/gi, '\\n')\n .replace(/<\\/div>/gi, '\\n')\n .replace(/<\\/pre>/gi, '\\n')\n .replace(/<pre[^>]*>/gi, '')\n .replace(/<a [^>]*>.*?<\\/a>/gi, '')\n .replace(/<[^>]+>/g, '')\n .replace(/ /g, ' ')\n .replace(/&/g, '&')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .trim();\n}\n\nfunction toIsoFromDDMMYYYY_HHMMSS(s) {\n const m = (s || '').match(/(\\d{2})\\.(\\d{2})\\.(\\d{4})\\s+(\\d{2}:\\d{2}:\\d{2})/);\n if (!m) return null;\n const [, dd, mm, yyyy, hhmmss] = m;\n return `${yyyy}-${mm}-${dd}T${hhmmss}Z`;\n}\n\nfunction normalize(s) {\n return (s || '')\n .toLowerCase()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .trim();\n}\n\nfunction parseNum(s) {\n if (s == null) return null;\n const t = String(s).replace(',', '.').trim();\n const n = Number(t);\n return Number.isFinite(n) ? n : null;\n}\n\nfunction detectConnectionType(subject, body, normAll) {\n const s = normalize(subject);\n const all = normAll || normalize(`${subject} ${body}`);\n\n if (\n s.includes('perdida de conexion') ||\n s.includes('p\u00e9rdida de conexi\u00f3n') ||\n all.includes('conexion perdida') ||\n all.includes('conexi\u00f3n perdida')\n ) return 'CONNECTION_LOST';\n\n if (\n s.includes('se restablece conexion') ||\n s.includes('se restablece conexi\u00f3n') ||\n all.includes('conexion restaurada') ||\n all.includes('conexi\u00f3n restaurada') ||\n all.includes('conexion restablecida') ||\n all.includes('conexi\u00f3n restablecida')\n ) return 'CONNECTION_RESTORED';\n\n return null;\n}\n\nfunction parseConnectionMinutesFromSubject(subject) {\n let m = (subject || '').match(/(\\d+)\\s*MIN/i);\n if (m) return Number(m[1]);\n\n m = (subject || '').match(/\\b1H\\b/i);\n if (m) return 60;\n\n m = (subject || '').match(/\\b(\\d+)\\s*H\\b/i);\n if (m) return Number(m[1]) * 60;\n\n return null;\n}\n\nfunction pickUnitCode(subject, body) {\n let m;\n\n subject = subject || '';\n body = body || '';\n\n m =\n subject.match(/\\(UNID\\s+([A-Z0-9-]+)\\)/i) ||\n body.match(/\\bUNID\\s+([A-Z0-9-]+)\\b/i) ||\n subject.match(/\\bUNID\\s+([A-Z0-9-]+)\\b/i);\n\n if (m) return m[1].toUpperCase();\n\n m = subject.match(/\\((V\\s*MOVIL\\s*\\d+)\\)/i);\n if (m) {\n return m[1].toUpperCase().replace(/\\s+/g, ' ').trim();\n }\n\n m =\n subject.match(/\\(TRANSIT\\s+([A-Z0-9-]+)\\)/i) ||\n body.match(/\\bTRANSIT\\s+([A-Z0-9-]+)\\b/i) ||\n subject.match(/\\bTRANSIT\\s+([A-Z0-9-]+)\\b/i);\n\n if (m) return m[1].toUpperCase();\n\n m = subject.match(/\\(([A-Z]{2,10}-[A-Z0-9-]+)\\)/i);\n if (m) return m[1].toUpperCase();\n\n m =\n subject.match(/\\b(T\\d{1,4})\\b/i) ||\n subject.match(/\\b(V\\d{1,4})\\b/i);\n\n if (m) return m[1].toUpperCase();\n\n return null;\n}\n\nfunction buildSourceHash(hashInput) {\n return crypto\n .createHash('sha256')\n .update(hashInput, 'utf8')\n .digest('hex');\n}\n\nfunction parseGpsEmail(input) {\n const subject = (input.subject || '').toString().trim();\n const textPlain = (input.textPlain || input.text || '').toString();\n const textHtml = (input.textHtml || '').toString();\n const rawContent = textHtml || textPlain || '';\n const body = textPlain || stripHtml(rawContent);\n\n const mailDate = input.mailDate || input.date || null;\n const email_message_id = input.email_message_id || input.messageId || null;\n const imap_uid = input.imap_uid || input.uid || null;\n\n const textAll = `${subject} ${body}`;\n const norm = normalize(textAll);\n\n const unit_code = pickUnitCode(subject, body);\n\n let event_time = null;\n let m = body.match(/(\\d{2}\\.\\d{2}\\.\\d{4}\\s+\\d{2}:\\d{2}:\\d{2})/);\n if (m) {\n event_time = toIsoFromDDMMYYYY_HHMMSS(m[1]);\n } else if (mailDate) {\n event_time = new Date(mailDate).toISOString();\n }\n\n let speed_kmh = null;\n m =\n body.match(/speed\\s+(\\d+(?:[.,]\\d+)?)\\s*km\\s*\\/?\\s*h/i) ||\n body.match(/velocidad\\s+de\\s+(\\d+(?:[.,]\\d+)?)\\s*km\\s*\\/?\\s*h/i);\n if (m) speed_kmh = parseNum(m[1]);\n\n let address_text = null;\n m =\n body.match(/near\\s+'([^']+)'/i) ||\n body.match(/cerca de\\s+'([^']+)'/i);\n if (m) address_text = m[1].trim();\n\n if (!address_text) {\n m =\n body.match(/near\\s+(.+?)(?:\\n|$)/i) ||\n body.match(/cerca de\\s+(.+?)(?:\\n|$)/i);\n if (m) address_text = m[1].trim().replace(/\\.$/, '');\n }\n\n let fuel_level_liters = null;\n let odometer_km = null;\n\n m = body.match(/Total\\s+de\\s+Combustible.*value\\s+of\\s+(\\d+(?:[.,]\\d+)?)\\s*l\\b/i);\n if (m) fuel_level_liters = parseNum(m[1]);\n\n m = body.match(/Odometro.*value\\s+of\\s+(\\d+(?:[.,]\\d+)?)\\s*km\\b/i);\n if (m) odometer_km = parseNum(m[1]);\n\n let event_type = 'OTHER';\n\n const connType = detectConnectionType(subject, body, norm);\n if (connType) {\n event_type = connType;\n } else {\n if (subject.toLowerCase().includes('sensor de combustible') && fuel_level_liters != null) {\n event_type = 'FUEL_LEVEL';\n } else if (subject.toLowerCase().includes('mileage sensor') && odometer_km != null) {\n event_type = 'MILEAGE';\n } else {\n const isDrain = norm.includes('descarga de combustible');\n const isFill = norm.includes('llenado de combustible');\n const isIdle = norm.includes('inactividad') || norm.includes('detenido');\n const isCasetaEnter =\n norm.includes('cruce de caseta') ||\n norm.includes('entro en caseta') ||\n norm.includes('entr\u00f3 en caseta');\n\n if (isDrain) event_type = 'FUEL_DRAIN';\n else if (isFill) event_type = 'FUEL_FILL';\n else if (isCasetaEnter) event_type = 'CASETA_ENTER';\n else if (isIdle) event_type = 'IDLE';\n }\n }\n\n let fuel_liters = null;\n m = textAll.match(/Llenado\\s+de\\s+Combustible\\s+(\\d+(?:[.,]\\d+)?)\\s*l\\b/i);\n if (m) fuel_liters = parseNum(m[1]);\n\n if (fuel_liters == null) {\n m = textAll.match(/descarga\\s+de\\s+combustible\\s+de\\s+(\\d+(?:[.,]\\d+)?)\\s*l\\b/i);\n if (m) fuel_liters = parseNum(m[1]);\n }\n\n let geofence_name = address_text || null;\n let geofence_kind = 'OTHER';\n const g = normalize(geofence_name || '');\n\n if (g.includes('caseta')) geofence_kind = 'CASETA';\n else if (g.includes('base') || g.includes('patio')) geofence_kind = 'BASE';\n else if (g.includes('planta') || g.includes('cedis')) geofence_kind = 'PLANTA';\n else if (g.includes('gasolinera') || g.includes('pemex') || g.includes('bp') || g.includes('shell')) {\n geofence_kind = 'GASOLINERA';\n }\n\n let connection_minutes = null;\n let connection_event = null;\n\n if (event_type === 'CONNECTION_LOST') {\n connection_event = 'LOST';\n connection_minutes = parseConnectionMinutesFromSubject(subject);\n } else if (event_type === 'CONNECTION_RESTORED') {\n connection_event = 'RESTORED';\n connection_minutes = parseConnectionMinutesFromSubject(subject);\n }\n\n const hash_input = [\n unit_code || '',\n event_time || '',\n event_type || '',\n geofence_name || '',\n speed_kmh ?? '',\n fuel_liters ?? '',\n fuel_level_liters ?? '',\n odometer_km ?? '',\n connection_event ?? '',\n connection_minutes ?? '',\n subject || '',\n email_message_id || ''\n ].join('|');\n\n const source_hash = buildSourceHash(hash_input);\n\n return {\n unit_code,\n event_time,\n event_type,\n geofence_name,\n geofence_kind,\n speed_kmh,\n fuel_liters,\n raw_subject: subject || null,\n raw_body: body || null,\n hash_input,\n source_hash,\n parse_ok: Boolean(event_time && unit_code),\n data: {\n address_text,\n geofence_kind,\n fuel_level_liters,\n odometer_km,\n parsed_speed_kmh: speed_kmh,\n connection_event,\n connection_minutes,\n fuel_liters,\n email_message_id,\n imap_uid,\n email_date: mailDate ? new Date(mailDate).toISOString() : null\n }\n };\n}\n\n// ---------------------------------------------------------------------------\n// Adaptador n8n: toma cada correo del trigger IMAP y lo convierte en la fila\n// que espera gps_event. Mismo parser que scripts/backfill_gps_event.js.\n// ---------------------------------------------------------------------------\n\nconst out = [];\n\nfor (const item of $input.all()) {\n const mail = item.json;\n\n const row = parseGpsEmail({\n subject: mail.subject || '',\n textPlain: mail.textPlain || mail.text || '',\n textHtml: mail.textHtml || mail.html || '',\n mailDate: mail.date || null,\n email_message_id: mail.messageId || (mail.metadata || {})['message-id'] || null,\n imap_uid: mail.uid || null,\n });\n\n out.push({\n json: {\n ...row,\n // JSONB va como texto: el nodo de Postgres no serializa objetos.\n data_json: JSON.stringify(row.data || {}),\n },\n });\n}\n\nreturn out;"
},
"id": "ParseGpsEmail",
"name": "Parse GPS Email",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-480,
0
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "parse-ok",
"leftValue": "={{ $json.parse_ok }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "IfParseOk",
"name": "IF parse_ok",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
-240,
0
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "INSERT INTO gps_event (\n unit_code, event_time, type, geofence_name, speed_kmh,\n raw_subject, raw_body, source_hash, geofence_kind, data\n)\nVALUES ($1, $2::timestamptz, $3, $4, $5, $6, $7, $8, $9, $10::jsonb)\nON CONFLICT (source_hash) DO UPDATE SET\n unit_code = EXCLUDED.unit_code,\n event_time = EXCLUDED.event_time,\n type = EXCLUDED.type,\n geofence_name = EXCLUDED.geofence_name,\n speed_kmh = EXCLUDED.speed_kmh,\n raw_subject = EXCLUDED.raw_subject,\n raw_body = EXCLUDED.raw_body,\n geofence_kind = EXCLUDED.geofence_kind,\n data = EXCLUDED.data\nRETURNING id;",
"options": {
"queryReplacement": "={{ $json.unit_code }}, {{ $json.event_time }}, {{ $json.event_type }}, {{ $json.geofence_name }}, {{ $json.speed_kmh }}, {{ $json.raw_subject }}, {{ $json.raw_body }}, {{ $json.source_hash }}, {{ $json.geofence_kind }}, {{ $json.data_json }}"
}
},
"id": "UpsertGpsEvent",
"name": "Postgres: Upsert gps_event",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.4,
"position": [
40,
-100
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {},
"id": "ReviewQueue",
"name": "Sin unidad u hora \u2192 revisar",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
40,
120
]
}
],
"connections": {
"Email Trigger (IMAP)": {
"main": [
[
{
"node": "Parse GPS Email",
"type": "main",
"index": 0
}
]
]
},
"Parse GPS Email": {
"main": [
[
{
"node": "IF parse_ok",
"type": "main",
"index": 0
}
]
]
},
"IF parse_ok": {
"main": [
[
{
"node": "Postgres: Upsert gps_event",
"type": "main",
"index": 0
}
],
[
{
"node": "Sin unidad u hora \u2192 revisar",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
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.
imappostgres
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
GPS: Ingesta IMAP → gps_event. Uses emailReadImap, postgres. Manual trigger; 5 nodes.
Source: https://github.com/DRAnguiano/gps-fleet-events/blob/main/n8n/imap_ingest_workflow.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
job.1_email_ingestion_pipeline. Uses postgres, emailReadImap, httpRequest. Manual trigger; 7 nodes.
05a Inbox: Monitor + Classify Replies. Uses emailReadImap, httpRequest, postgres, emailSend. Manual trigger; 5 nodes.
If you are a postmaster or you manage email server, you can set up DKIM and SPF records to ensure that spoofing your email address is hard. On your domain you can also set up DMARC record to receive X
This workflow automates URL reporting to Spamhaus based on incoming spam/phishing sample emails. It watches one or more IMAP folders, extracts URLs from each email body, removes duplicates and common
Email. Uses emailReadImap, theHive, cortex. Manual trigger; 15 nodes.