Cloudflare Email worker to HomeAssistant event

Wanted to share the cloudflare email worker i made to stop using the IMAP integration for constant polling that can be created via the cloudflare dashboard and then set for the catch-all address. Sadly normal routing addresses need to be disabled for it but theres a dictionary in it that handles forwarding/dropping certain addresses.

Cloudflare Email Worker JS Code:

const HASS_URL = 'https://your.internet.accessible.hass.url/';
const HASS_EVENT = 'email';
const HASS_TOKEN = "<https://my.home-assistant.io/redirect/profile_security/>";
const routingRules = {
  '*': ['[email protected]'],
  '[email protected]': ['[email protected]','[email protected]'],
  '[email protected]':[],
};
export default {
  /**
   * @param {ForwardableEmailMessage} message
   * @param {object} env
   * @param {object} ctx
   */
  async email(message, env, ctx) {
    var routedTo = await handleRouting(message, env, ctx, routingRules);
    try {
      const rawContent = await new Response(message.raw).text();
      const decodedBody = decodeEmailBody(rawContent);
      const eventData = {
        timestamp: new Date().toISOString(),
        /*message: message,
        env: env,
        ctx: ctx,*/
        from: message.from,
        sender: message.from,
        to: message.to,
        username: message.to,
        reciever: message.to,
        routed_to: routedTo,
        subject: message.headers.get("subject") || "No Subject",
        size: message.rawSize,
        content: rawContent, // .replaceAll("\\r\\n","\n")
        body: decodedBody,
        body_plain: htmlToPlainText(decodedBody),
        headers: Array.from(message.headers.entries()).reduce(
          (acc, [key, value]) => {
            acc[key] = value;
            return acc;
          },
          {}
        ),
      };
      const response = await fetch(`${HASS_URL}api/events/${HASS_EVENT}`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Accept: "application/json",
          Authorization: `Bearer ${HASS_TOKEN}`, // Using environment variable
        },
        body: JSON.stringify(eventData),
      });
      if (!response.ok) {
        throw new Error(`Failed to forward email: ${response.statusText}`);
      }
      console.log("Event sent to Home Assistant");
    } catch (error) {
      console.error("Error sending HomeAssistant event:", error);
      throw error;
    }
  },
};
/**
 * @param {ForwardableEmailMessage} message
 * @param {object} env
 * @param {object} ctx
 * @returns {Promise<string[]>} forwarded-to
 */
async function handleRouting(message, env, ctx, rules) {
  const recipient = message.to.toLowerCase();
  const rule = rules[recipient];
  if (rule) {
    // Check if it's an empty array (drop rule)
    if (Array.isArray(rule) && rule.length === 0) {
      message.setReject("Rejected by routing rule");
      console.warn(`Rejected mail from ${message.from} to ${message.to}`);
      return [];
    }
    const forwardTo = Array.isArray(rule) ? rule : [rule];
    for (const address of forwardTo) {
      await message.forward(address);
    }
    console.log(
      `Forwarded mail from ${message.from} meant for ${
        message.to
      } to ${forwardTo.join(", ")}`
    );
    return forwardTo;
  }
  // Fallback to wildcard rule
  if (rules["*"]) {
    const forwardTo = Array.isArray(rules["*"]) ? rules["*"] : [rules["*"]];
    for (const address of forwardTo) {
      await message.forward(address);
    }
    console.log(
      `Forwarded mail from ${message.from} meant for ${
        message.to
      } to ${forwardTo.join(", ")}`
    );
    return forwardTo;
  }
  return [];
}
function splitOnSecondLastDoubleNewline(str) {
  const doubleNewline = "\r\n\r\n";
  const positions = [];
  let index = str.indexOf(doubleNewline);
  while (index !== -1) {
    positions.push(index);
    index = str.indexOf(doubleNewline, index + 1);
  }
  if (positions.length < 2) {
    return [str];
  }
  const secondLastPos = positions[positions.length - 2];
  const before = str.substring(0, secondLastPos);
  const after = str.substring(secondLastPos + doubleNewline.length);
  return [before, after];
}
/**
 * Decode base64-encoded email body if detected
 * @param {string} rawContent - The raw email content
 * @returns {string} Decoded body content
 */
function decodeEmailBody(rawContent) {
  const body = splitOnSecondLastDoubleNewline(rawContent)[1]?.trim() || "";
  // Check if the body or a part of it is base64 encoded
  // Look for Content-Transfer-Encoding: base64 header followed by content
  const base64Pattern =
    /Content-Transfer-Encoding:\s*base64\r?\n\r?\n([\s\S]+?)(?:\r?\n--|\r?\n\r?\n--)/gi;
  const matches = [...rawContent.matchAll(base64Pattern)];
  if (matches.length > 0) {
    // Get the last base64 encoded part (usually the HTML content)
    const base64Content = matches[matches.length - 1][1]
      .trim()
      .replace(/\r?\n/g, "");
    try {
      // Decode base64
      const decoded = atob(base64Content);
      return decoded;
    } catch (error) {
      console.warn("Failed to decode base64 content:", error);
      return body;
    }
  }
  return body;
}

/**
 * Convert HTML to plain text by stripping tags and removing script/style content
 * @param {string} html - The HTML content to convert
 * @returns {string} Plain text version
 */
function htmlToPlainText(html) {
  if (!html) return "";

  let text = html;

  // Remove all content inside <style*> tags (case insensitive)
  text = text.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");

  // Remove all content inside <script*> tags (case insensitive)
  text = text.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");

  // Strip all remaining HTML tags
  text = text.replace(/<[^>]+>/g, "");

  // Decode common HTML entities
  text = text
    .replace(/&nbsp;/g, " ")
    .replace(/&amp;/g, "&")
    .replace(/&lt;/g, "<")
    .replace(/&gt;/g, ">")
    .replace(/&quot;/g, '"')
    .replace(/&#39;/g, "'")
    .replace(/&apos;/g, "'");

  // Clean up extra whitespace
  text = text.replace(/\s+/g, " ").trim();

  return text;
}

Example Automation:

alias: Email Notifications
description: ""
triggers:
  - trigger: event
    event_type: imap_content
    enabled: false
  - trigger: event
    event_type: email
conditions: []
actions:
  - action: notify.blus_notify_devices
    metadata: {}
    data:
      message: |-
        {{trigger.event.data.subject}}

        {{trigger.event.data.body_plain}}
      title: >-
        E-Mail from {{trigger.event.data.sender}} to
        {{trigger.event.data.username}}
mode: single

1 Like