Skip to content

Customization

This guide shows how to customize the email integration's behavior by implementing custom tasks and message processors. You can intercept incoming messages, add custom logic, and integrate with other systems while leveraging the standard email integration functionality.

Custom Incoming Message Task

When a new email arrives at EmailEngine, a webhook will be sent to the email integration to handle it. The handling is executed in a task that can be overridden by a custom one using Application Config. The same task will also be used by the recovery functions.

Getting Started

  1. Generate a new task in your solution using lime-project generate task.
  2. Implement your custom logic in the generated task file.
  3. Register the task to override the standard incoming webhook task by adding it to the Optional Application Configuration.
  4. Restart the task handler to apply the changes.

Example Webhook Message

The incoming message looks something like below.

{
  "id": "AAAAAQAACLY",
  "uid": 2230,
  "path": "INBOX",
  "date": "2024-09-05T12:39:38.000Z",
  "flags": [],
  "unseen": true,
  "size": 66202,
  "subject": "Want to buy a pizza [1234]",
  "from": { "name": "Kim Young", "address": "kim@rocketfactory.kp" },
  "to": [
    {
      "name": "Support",
      "address": "support@lime.tech"
    }
  ],
  "messageId": "<AS8PR08MB89955D313A2F291643AD9E6C9R2D2@AS8PR08MB8995.japrd368.prof.inlook.com>",
  "text": {
    "id": "AAAAAQAACLaTkaExkaEykA",
    "encodedSize": { "plain": 31, "html": 1300 }
  },
  "specialUse": "\\Inbox",
  "messageSpecialUse": "\\Inbox",
  "seemsLikeNew": true
}

Note

id and account_id are the internal EmailEngine IDs that need to be passed to the standard email integration import function. messageId is the globally unique Message-ID. The text/html of the message is not included for performance reasons.

Example: Pizza Handler

For the solution "Pizza", a new task called incoming was created. Its purpose is to filter out emails containing the word "pizza" in the subject. These should not result in conversations, but rather just create history notes. If no "pizza" is present in the subject, the standard function import_message() that imports messages will be used instead.

Application Configuration

In the Application Configuration, the following parameter was added:

incoming_message_task: solution_pizza.tasks.tasks.incoming

Task Code

File: solution_pizza/tasks/tasks.py

from lime_task import task
from limepkg_email.incoming_message import import_message

import logging


logger = logging.getLogger(__name__)


@task
def incoming(app, message, account_id):
    if "pizza" in message.get("subject", ""):
        logger.info("Found pizza in subject, creating history note")

        history = app.limetypes.history()
        history.properties.type.set_by_key("comment")
        history.properties.note.value = (
            f'{message["from"]["name"]} wants to buy a pizza!'
        )
        uow = app.unit_of_work()
        uow.add(history)
        uow.commit()
    else:
        logger.info("No pizza present in subject, starting normal import")

        import_message(
            app,
            message,
            account_id,
        )

Testing

  1. Restart the task handler after every code change.
  2. Send an email with the word "pizza" in the subject and expect a history note to be created.
  3. Send an email without "pizza" in the subject and expect a conversation message to be created.

Example: Inbox Migration

We want to offer an easy way to migrate from Lime Inbox to this integration. Unfortunately, the systems are too different to just migrate the data. Instead, we need to continue ongoing conversations with Inbox and create new conversations with the standard import task. This means that both integrations will run in parallel until all Inbox conversations are completed. At that point the Inbox package and this custom task can be removed. Talk with your customer about the average time of their conversations to estimate when the migration can be completed.

Application Configuration

Same as in the earlier example, the following parameter is added:

incoming_message_task: solution_your_favorite_inbox_customer.tasks.tasks.incoming

Task Code

File: solution_your_favorite_inbox_customer/tasks/tasks.py

import logging

from lime_application import LimeApplication
from lime_task import task
from limepkg_email.incoming_message import import_message
from limepkg_ms_inbox.limepkg_email.process import (
    check_if_message_is_for_inbox,
    process_inbox_message,
)

from solution_your_favorite_inbox_customer.endpoints.lime_inbox import _handle_email_message

logger = logging.getLogger(__name__)


@task
def incoming(app: LimeApplication, message: dict, account_id: str) -> None:
    """
    Handles incoming email for transition period of Lime inbox/Communication flow ->
    Email integration. Checks if MS Inbox or Email engine should handle it and
    sends it to the correct function.
    Args:
        app (LimeApplication: The LimeApplication instance
        message (dict): Incoming data from webhook
        account_id (str): ID of the account
    """
    subject = message.get("subject", "")
    message_id = message["messageId"]
    logger.info(f"[handle_incoming_email.incoming] Message id [{message_id}]")
    if check_if_message_is_for_inbox(app, subject):
        logger.info(f"Message id [{message_id}] was identified as a Lime Inbox message")
        subscription_email = message["to"][0]["address"]
        process_inbox_message(
            app, subscription_email, message_id, _handle_email_message
        )
    else:
        import_message(
            app,
            message_payload=message,
            account_id=account_id,
        )

Example: Custom Message Processor

In some customer cases, the standard import processor will need tweaks. You can achieve that by implementing your version of the limepkg_email.incoming_message.standard.StandardIncomingMessageProcessor. In the example below, everything is the same as the standard processor except that automatic replies won't be sent to recipients on CC. Notice how you only need to override the methods with your custom logic. Check out the limepkg_email.incoming_message.standard.StandardIncomingMessageProcessor for more methods to override.

Rules for Implementations

These rules are necessary to allow future updates to the standard processor without breaking your custom implementation. If you break them, there's no guarantee that your custom implementation will work with future versions.

  • Don't import other functions or classes used by the standard processor. These might change or move in the future.
  • Fulfill the function's promise. For example, the create_conversation function should create a Conversation object and return it. It's your responsibility to add any objects you create to the unit of work. You may use the self.uow property on the processor class for that.
  • We recommend that you stay as close to the standard processor as possible. Completely overwriting a function without calling super() is done at your own risk and might lead to something important missing on the imported conversation. Test your process changes thoroughly.
  • You may access any public properties and methods on the processor class (those not prefixed with _). Private members (prefixed with _) are internal implementation details and may change without notice.

Application Configuration

incoming_message_task: solution_pizza.tasks.tasks.incoming

Task Code

import logging
from lime_application import LimeApplication
from lime_task import task

from limepkg_email.incoming_message import import_message
from limepkg_email.incoming_message.standard import (
    StandardIncomingMessageProcessor,
)


logger = logging.getLogger(__name__)


class CustomIncomingMessageProcessor(StandardIncomingMessageProcessor):
    """
    Follows the StandardIncomingMessageProcessor,
    but don't send automatic replies to recipients on CC.

    Also whitelist automatic replies sent from "order@foo.bar", to make sure these emails are imported into Lime CRM.
    """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def send_automatic_reply(self):
        """
        Blacklist CC recipients from receiving automatic replies.

        Note: CC recipients are still active and will get automatic updates.
        """
        cc_addresses = [
            cc_receiver.address for cc_receiver in self.message.cc_receivers
        ]

        super().send_automatic_reply(blacklist=cc_addresses)

    def is_automatic_email(self) -> bool:
        """
        Whitelist automatic replies sent from "order@foo.bar"
        """
        whitelist = ["order@foo.bar"]
        if self.message.sender.address in whitelist:
            return False

        return super().is_automatic_email()


@task
def incoming(app: LimeApplication, message: dict, account_id: str):
    """
    Processes incoming messages using the CustomIncomingMessageProcessor.
    Blacklists automatic replies for recipients on CC.
    """
    import_message(app, message, account_id, CustomIncomingMessageProcessor)