AutomationFlowsEmail & Gmail › Gps: Ingesta IMAP → Gps_event

Gps: Ingesta IMAP → Gps_event

GPS: Ingesta IMAP → gps_event. Uses emailReadImap, postgres. Manual trigger; 5 nodes.

Manual trigger★★★★☆ complexity5 nodesEmail Read ImapPostgres
Email & Gmail Trigger: Manual Nodes: 5 Complexity: ★★★★☆ Added:

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": "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(/&nbsp;/g, ' ')\n    .replace(/&amp;/g, '&')\n    .replace(/&quot;/g, '\"')\n    .replace(/&#39;/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.

Pro

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 →

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

job.1_email_ingestion_pipeline. Uses postgres, emailReadImap, httpRequest. Manual trigger; 7 nodes.

Postgres, Email Read Imap, HTTP Request
Email & Gmail

05a Inbox: Monitor + Classify Replies. Uses emailReadImap, httpRequest, postgres, emailSend. Manual trigger; 5 nodes.

Email Read Imap, HTTP Request, Postgres +1
Email & Gmail

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

Email Read Imap, MySQL, Compression +3
Email & Gmail

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 Read Imap, HTTP Request
Email & Gmail

Email. Uses emailReadImap, theHive, cortex. Manual trigger; 15 nodes.

Email Read Imap, The Hive, Cortex