// Follow this setup guide to integrate the Deno language server with your editor: // https://deno.land/manual/getting_started/setup_your_environment // This enables autocomplete, go to definition, etc. // Setup type definitions for built-in Supabase Runtime APIs import "jsr:@supabase/functions-js/edge-runtime.d.ts"; import { addNoteToContact, findActiveSaleByEmail } from "./addNoteToContact.ts"; import { getForwardedMailContent, stripSubjectForwardingPrefix, } from "./forwardedParser.ts"; import { extractMailContactData } from "./extractMailContactData.ts"; import { getExpectedAuthorization } from "./getExpectedAuthorization.ts"; import { getNoteContent } from "./getNoteContent.ts"; import { extractAndUploadAttachments, removeUploadedAttachments, } from "./extractAndUploadAttachments.ts"; import { supabaseAdmin } from "../_shared/supabaseAdmin.ts"; const webhookUser = Deno.env.get("POSTMARK_WEBHOOK_USER"); const webhookPassword = Deno.env.get("POSTMARK_WEBHOOK_PASSWORD"); const INBOUND_EMAIL = (Deno.env.get("VITE_INBOUND_EMAIL") || "").toLowerCase(); if (!webhookUser || !webhookPassword) { throw new Error( "Missing POSTMARK_WEBHOOK_USER or POSTMARK_WEBHOOK_PASSWORD env variable", ); } const rawAuthorizedIPs = Deno.env.get("POSTMARK_WEBHOOK_AUTHORIZED_IPS"); if (!rawAuthorizedIPs) { throw new Error("Missing POSTMARK_WEBHOOK_AUTHORIZED_IPS env variable"); } Deno.serve(async (req) => { let response: Response | undefined; response = checkRequestTypeAndHeaders(req); if (response) return response; const json = await req.json(); response = checkBody(json); if (response) return response; const { FromFull, Attachments } = json; let { ToFull, TextBody, Subject } = json; const salesEmail = (FromFull.Email || "").toLowerCase(); if (!salesEmail) { // Return a 403 to let Postmark know that it's no use to retry this request // https://postmarkapp.com/developer/webhooks/inbound-webhook#errors-and-retries return new Response( `Could not extract sales email from FromFull: ${FromFull}`, { status: 403 }, ); } const allSales = await supabaseAdmin .from("sales") .select("email, secondary_emails"); if (allSales.error) { console.error("Could not fetch sales emails:", allSales.error); return new Response("Could not fetch sales from database", { status: 500 }); } const salesEmails = allSales.data ?.flatMap((s: { email: string; secondary_emails: string[] }) => [ s.email, ...s.secondary_emails, ]) .map((email: string) => email.toLowerCase()) ?? []; const firstToEmail = (ToFull[0]?.Email || "").toLowerCase(); // If we have an INBOUND_EMAIL and the email is sent only to the inbound email address, and the sender is a known sales email, // then we can try to extract the real recipient email from the body of the email if ( INBOUND_EMAIL && ToFull.length === 1 && firstToEmail === INBOUND_EMAIL && salesEmails.includes(salesEmail) ) { const emailRegex = /[\w.+%-]+@[\w.-]+\.[a-zA-Z]{2,}/g; const emailsInBody = TextBody.match(emailRegex) || []; const candidateEmails = emailsInBody .map((email: string) => email.toLowerCase()) .filter( (email: string) => email !== INBOUND_EMAIL && !salesEmails?.includes(email), ); if (candidateEmails.length > 0) { ToFull = [ { Email: candidateEmails[0], Name: "", }, ]; } else { // Return a 403 to let Postmark know that it's no use to retry this request // https://postmarkapp.com/developer/webhooks/inbound-webhook#errors-and-retries return new Response( `Could not extract recipient email from transferred email body.`, { status: 403 }, ); } TextBody = getForwardedMailContent(TextBody); Subject = stripSubjectForwardingPrefix(Subject); } const noteContent = getNoteContent(Subject, TextBody); const contacts = extractMailContactData(ToFull); const { data: sales, error: fetchSalesError } = await findActiveSaleByEmail(salesEmail); if (fetchSalesError) { console.error("Could not fetch the sender's sale:", fetchSalesError); return new Response( `Could not fetch sales from database, email: ${salesEmail}`, { status: 500 }, ); } if (!sales) { // Return a 403 to let Postmark know that it's no use to retry this request // https://postmarkapp.com/developer/webhooks/inbound-webhook#errors-and-retries return new Response( `Unable to find (active) sales in database, email: ${salesEmail}`, { status: 403 }, ); } const attachments = await extractAndUploadAttachments(Attachments); const failedContacts: string[] = []; for (const { firstName, lastName, email, domain, companyName, website, } of contacts) { if (!email) { // Return a 403 to let Postmark know that it's no use to retry this request // https://postmarkapp.com/developer/webhooks/inbound-webhook#errors-and-retries return new Response(`Could not extract email from ToFull: ${ToFull}`, { status: 403, }); } const errorResponse = await addNoteToContact({ sales, salesEmail, email, domain, firstName, lastName, noteContent, attachments, companyName, website, }); if (errorResponse) { console.error( `Could not add the note for ${email}: ${await errorResponse.text()}`, ); failedContacts.push(email); } } if (failedContacts.length) { // A partial failure answers 403 so Postmark does not redeliver: the notes // already written are not idempotent and would be duplicated. When every // contact failed there is no note to duplicate, so a retry is safe. // https://postmarkapp.com/developer/webhooks/inbound-webhook#errors-and-retries const status = failedContacts.length === contacts.length ? 500 : 403; if (status === 500) { await removeUploadedAttachments(attachments); } return new Response( `Could not add the note for: ${failedContacts.join(", ")}`, { status }, ); } return new Response("OK"); }); const checkRequestTypeAndHeaders = (req: Request) => { // Only allow known IP addresses // We can use the x-forwarded-for header as it is populated by Supabase // https://supabase.com/docs/guides/api/securing-your-api#accessing-request-information const forwardedFor = req.headers.get("x-forwarded-for"); if (!forwardedFor) { return new Response("Unauthorized", { status: 401 }); } const ips = forwardedFor.split(",").map((ip) => ip.trim()); const authorizedIPs = rawAuthorizedIPs .split(",") .map((ip: string) => ip.trim()); if (!ips.some((ip) => authorizedIPs.includes(ip))) { return new Response("Unauthorized", { status: 401 }); } // Only allow POST requests if (req.method !== "POST") { return new Response(null, { status: 405 }); } // Check the Authorization header const expectedAuthorization = getExpectedAuthorization( webhookUser, webhookPassword, ); const authorization = req.headers.get("Authorization"); if (authorization !== expectedAuthorization) { return new Response("Unauthorized", { status: 401 }); } }; // deno-lint-ignore no-explicit-any const checkBody = (json: any) => { const { ToFull, FromFull, Subject, TextBody } = json; // In case of incorrect request data, we // return a 403 to let Postmark know that it's no use to retry this request // https://postmarkapp.com/developer/webhooks/inbound-webhook#errors-and-retries if (!ToFull || !ToFull.length) return new Response("Missing parameter: ToFull", { status: 403 }); if (!FromFull) return new Response("Missing parameter: FromFull", { status: 403 }); if (!Subject) return new Response("Missing parameter: Subject", { status: 403 }); if (!TextBody) return new Response("Missing parameter: TextBody", { status: 403, }); }; /* To invoke locally: 1. Run `make start` 2. Make sure to have a Sales with email "support@postmarkapp.com" (create it if needed) 3. OPTIONAL: Create a Contact with email "firstname.lastname@marmelab.com" 4. In another terminal, run `make start-supabase-functions` 5. In another terminal, make an HTTP request: curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/postmark' \ --header 'Content-Type: application/json' \ --header 'Authorization: Basic dGVzdHVzZXI6dGVzdHB3ZA==' \ --data '{ "FromName": "Postmarkapp Support", "From": "support@postmarkapp.com", "FromFull": { "Email": "support@postmarkapp.com", "Name": "Postmarkapp Support", "MailboxHash": "" }, "To": "\"Firstname Lastname\" ", "ToFull": [ { "Email": "firstname.lastname@marmelab.com", "Name": "Firstname Lastname", "MailboxHash": "SampleHash" } ], "Cc": "\"First Cc\" , secondCc@postmarkapp.com", "CcFull": [ { "Email": "firstcc@postmarkapp.com", "Name": "First Cc", "MailboxHash": "" }, { "Email": "secondCc@postmarkapp.com", "Name": "", "MailboxHash": "" } ], "Bcc": "\"First Bcc\" , secondbcc@postmarkapp.com", "BccFull": [ { "Email": "firstbcc@postmarkapp.com", "Name": "First Bcc", "MailboxHash": "" }, { "Email": "secondbcc@postmarkapp.com", "Name": "", "MailboxHash": "" } ], "OriginalRecipient": "firstname.lastname@marmelab.com", "Subject": "Test subject", "MessageID": "73e6d360-66eb-11e1-8e72-a8904824019b", "ReplyTo": "replyto@postmarkapp.com", "MailboxHash": "SampleHash", "Date": "Fri, 1 Aug 2014 16:45:32 -04:00", "TextBody": "This is a test text body.", "HtmlBody": "

This is a test html body.

", "StrippedTextReply": "This is the reply text", "Tag": "TestTag", "Headers": [ { "Name": "X-Header-Test", "Value": "" }, { "Name": "X-Spam-Status", "Value": "No" }, { "Name": "X-Spam-Score", "Value": "-0.1" }, { "Name": "X-Spam-Tests", "Value": "DKIM_SIGNED,DKIM_VALID,DKIM_VALID_AU,SPF_PASS" } ], "Attachments": [ { "Name": "test.txt", "Content": "VGhpcyBpcyBhdHRhY2htZW50IGNvbnRlbnRzLCBiYXNlLTY0IGVuY29kZWQu", "ContentType": "text/plain", "ContentLength": 45 } ] }' To trigger the email forwarding feature, you can change the "To" and "ToFull" fields to have the INBOUND_EMAIL, and add an email address that is neither a sales nor the INBOUND_EMAIL, for example: curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/postmark' \ --header 'Content-Type: application/json' \ --header 'Authorization: Basic dGVzdHVzZXI6dGVzdHB3ZA==' \ --data '{ "FromName": "Postmarkapp Support", "MessageStream": "inbound", "From": "support@postmarkapp.com", "FromFull": { "Email": "support@postmarkapp.com", "Name": "Postmarkapp Support", "MailboxHash": "" }, "To": "2aff30e603e54dc3eb556bd9e03ee099@inbound.postmarkapp.com", "ToFull": [ { "Email": "2aff30e603e54dc3eb556bd9e03ee099@inbound.postmarkapp.com", "Name": "", "MailboxHash": "" } ], "Cc": "", "CcFull": [], "Bcc": "", "BccFull": [], "OriginalRecipient": "2aff30e603e54dc3eb556bd9e03ee099@inbound.postmarkapp.com", "Subject": "Fwd: Test for forwarding mail", "MessageID": "32dcbecb-57d0-476e-9591-c747808cb599", "ReplyTo": "", "MailboxHash": "", "Date": "Thu, 5 Mar 2026 10:41:26 +0100", "TextBody": "---------- Forwarded message ---------\nFrom : Original Recipient \nDate: Fri, 1 Aug 2014 16:45:32 -04:00\nSubject: Test for forwarding mail\nTo: Postmarkapp Support \n\n\nThe transferred message body\n", "HtmlBody": "


---------- Forwarded message ---------
From: Original Recipient <original.recipient@company.com>
Date: Fri, 1 Aug 2014 16:45:32 -04:00
Subject: Test for forwarding mail
To: Postmarkapp Support <support@postmarkapp.com>


The transferred message body
\n
\n", "StrippedTextReply": "", "Tag": "", "Headers": [ { "Name": "X-Header-Test", "Value": "" }, { "Name": "X-Spam-Status", "Value": "No" }, { "Name": "X-Spam-Score", "Value": "-0.1" }, { "Name": "X-Spam-Tests", "Value": "DKIM_SIGNED,DKIM_VALID,DKIM_VALID_AU,SPF_PASS" } ], "Attachments": [] }' */