Skip to content

Customization

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

Filtering Emails Before Import

If you want to prevent certain emails from being imported into Lime CRM entirely — for example, emails from a specific sender, domain, or containing a keyword in the subject line — we recommend configuring rules directly on your mail server rather than using a custom task.

Server-side filtering is simpler, more reliable, and keeps your Lime CRM solution clean. By doing so, the rules can be created, read, and maintained by the customer themselves. Most mail platforms support this out of the box:

  • Microsoft Exchange / Outlook: Use mail flow rules (transport rules) in the Exchange Admin Center
  • Gmail / Google Workspace: Use routing rules in the Google Admin Console

Move or delete filtered emails before they reach the monitored inbox folder — the email integration will never see them.

Note

When to use a custom task instead: Custom tasks are meant for cases where the filtering logic depends on data inside Lime CRM, or where you want to do something with the email rather than simply discard it (e.g., route it to a different conversation parent, create a history note, or trigger an external system).

Custom Incoming Message Processor

When a new email arrives, it goes through a multi-phase import pipeline with full state tracking and retry support. You can customize message handling by extending the StandardIncomingMessageProcessor class and configuring it via Application Config.

Getting Started

The incoming_message_processor setting and pre_process only work with the new import flow. See Migrating to the New Import Flow.

  1. Create a new Python module in your solution (e.g., solution_pizza/message_processors.py).
  2. Implement a class that extends StandardIncomingMessageProcessor.
  3. Override the pre_process method to filter or route messages, and/or override individual processing methods to customize behavior.
  4. Register your processor in Application Configuration with the config.limepkg_email.incoming_message_processor key.
  5. Restart the task handler to apply the changes.

Deprecated: Custom Incoming Message Task

The config.limepkg_email.incoming_message_task configuration parameter is deprecated and will be removed in a future version. It bypasses the state-tracked import pipeline, losing retry/recovery support and visibility. Migrate to config.limepkg_email.incoming_message_processor instead. See Migration from incoming_message_task at the end of this document.

Parsed Message Interface

Your pre_process method receives a parsed email message with full access to all message properties:

  • message.subject - Email subject line
  • message.sender - Sender address and name
  • message.to_receivers - List of To recipients
  • message.cc_receivers - List of CC recipients
  • message.html - HTML body content
  • message.plain_text - Plain text body content
  • message.message_id - Unique Message-ID header
  • message.date - Message date
  • message.attachments - List of attachments
  • message.headers - Full email headers
  • message.eml_file - The original email as an EML file

See the IncomingMessageInterface for the complete API.

Example: Pizza Handler

For the solution "Pizza", a custom processor was created 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 import pipeline continues.

Application Configuration

In the Application Configuration, the following parameter was added:

config:
  limepkg_email:
    incoming_message_processor: solution_pizza.message_processors.PizzaProcessor

Processor Code

File: solution_pizza/message_processors.py

import logging

from limepkg_email.incoming_message.processors import (
    StandardIncomingMessageProcessor,
)

logger = logging.getLogger(__name__)


class PizzaProcessor(StandardIncomingMessageProcessor):
    """
    Custom processor that creates history notes for pizza-related emails
    instead of importing them as conversations.
    """

    def pre_process(self) -> bool:
        """
        Filter out pizza-related emails and create history notes instead.

        Returns:
            False to skip conversation import for pizza emails,
            True to continue with standard import for other emails.
        """
        if "pizza" in self.message.subject.lower():
            logger.info(
                f"Found pizza in subject '{self.message.subject}', "
                f"creating history note"
            )

            history = self.app.limetypes.history()
            history.properties.type.set_by_key("comment")
            history.properties.note.value = (
                f"{self.message.sender.name} wants to buy a pizza!"
            )
            uow = self.app.unit_of_work()
            uow.add(history)
            uow.commit()

            return False  # Skip conversation import

        logger.info(
            f"No pizza in subject '{self.message.subject}', "
            f"starting normal import"
        )
        return True  # Continue with standard import

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. This means that both integrations will run in parallel until all Inbox conversations are completed. At that point the Inbox package and this custom processor 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:

config:
  limepkg_email:
    incoming_message_processor: solution_your_favorite_inbox_customer.message_processors.InboxMigrationProcessor

Processor Code

File: solution_your_favorite_inbox_customer/message_processors.py

import logging

from limepkg_email.incoming_message.processors import (
    StandardIncomingMessageProcessor,
)
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__)


class InboxMigrationProcessor(StandardIncomingMessageProcessor):
    """
    Handles incoming email for transition period of Lime Inbox →
    Email integration. Routes messages to the correct system.
    """

    def pre_process(self) -> bool:
        """
        Check if message belongs to Lime Inbox

        Returns:
            False if routed to Inbox (skip email integration import),
            True to continue with standard email integration import.
        """
        if check_if_message_is_for_inbox(self.app, self.message.subject):
            logger.info(
                f"Message [{self.message.message_id}] "
                f"was identified as a Lime Inbox message"
            )
            process_inbox_message(
                self.app,
                self.account.address,
                self.message.message_id,
                _handle_email_message,
            )
            return False  # Skip email integration import

        return True  # Continue with standard import

Example: Custom Processing Steps

In some customer cases, the standard import processor needs tweaks. You can customize processing by extending StandardIncomingMessageProcessor and overriding specific methods. In the example below, automatic replies are not sent to recipients on CC. Notice how you only need to override the methods with your custom logic.

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

config:
  limepkg_email:
    incoming_message_processor: solution_pizza.message_processors.CustomIncomingMessageProcessor

Processor Code

File: solution_pizza/message_processors.py

import logging

from limepkg_email.incoming_message.processors import (
    StandardIncomingMessageProcessor,
)

logger = logging.getLogger(__name__)


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

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

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

        Note: CC recipients are still active and will get automatic updates.

        Return True if a reply was sent and False if it was skipped.
        """
        cc_addresses = [
            cc_receiver.address for cc_receiver in self.message.cc_receivers
        ]

        return 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()

Example: Combining Pre-Process Filter and Custom Processing

You can combine both patterns: use pre_process to filter messages AND override processing methods to customize behavior:

class AdvancedProcessor(StandardIncomingMessageProcessor):
    """
    Filter spam in pre_process, customize automatic replies in processing.
    """

    def pre_process(self) -> bool:
        """Skip spam emails entirely."""
        if is_spam(self.message):
            logger.info(f"Spam detected, skipping: {self.message.subject}")
            return False
        return True

    def send_automatic_reply(self) -> bool:
        """Don't send automatic replies to CC recipients."""
        cc_addresses = [r.address for r in self.message.cc_receivers]
        return super().send_automatic_reply(blacklist=cc_addresses)

Migration from incoming_message_task

If you're currently using the deprecated config.limepkg_email.incoming_message_task configuration, follow this guide to migrate to config.limepkg_email.incoming_message_processor.

Turn on the new import flow first

incoming_message_processor is only read by the new import flow. With the old flow, setting it disables your custom task and every email gets the standard import.

On-premise installations

On-premise, the new import flow is available from Lime CRM Bodensee (limepkg-email 2.117.x), where it has to be turned on with the feature switch. On Arresø (limepkg-email 2.57.x) or older, keep using incoming_message_task with the old import flow.

Migration Steps

  1. Make sure the new import flow is turned on.
  2. Use the processor class your task already passed to import_message(), or create a new class that extends StandardIncomingMessageProcessor.
  3. If your task had logic before calling import_message(), move it into pre_process on that class. Return True to import the email, or False if you've handled it yourself and it shouldn't be imported. Either way, the email has already been moved to the processed folder by the time pre_process runs.
  4. Read message data from self.message instead of the webhook payload. If your task downloaded the email file with download_raw_message(), use self.message.eml_file instead. See Reading Message Data.
  5. Update imports that use deprecated paths.
  6. In Application Config, remove incoming_message_task and set incoming_message_processor to the full import path of your processor class, e.g. solution_pizza.message_processors.PizzaProcessor. Note that it points to the class, not to a task function.
  7. Delete the old task.
  8. Deploy the solution and test your customization by sending an email to one of the email accounts configured in Lime Admin.

Reading Message Data

Your task received the raw EmailEngine webhook payload as a dict. The processor gets a parsed message instead (see Parsed Message Interface), so dict lookups need to be rewritten:

In the task In the processor
message["subject"] self.message.subject
message["messageId"] self.message.message_id
message["from"]["address"] self.message.sender.address
message["to"][0]["address"] self.message.to_receivers[0].address
message["cc"] self.message.cc_receivers
account_id self.account.id
download_raw_message(app, account_id, id) self.message.eml_file

The address of the mailbox the email arrived in is self.account.address.

Things That Work Differently

  • pre_process runs after the email has been downloaded and moved to the processed folder. Returning False doesn't leave the email in the inbox.
  • If pre_process returns True, add any objects you create to self.uow. They're then saved together with the imported email, so if the import fails and is retried, nothing is created twice. Anything you commit yourself or send to another system runs again on retry.
  • If pre_process returns False, nothing else commits self.uow, so commit your own changes (as in the Pizza example).

Deprecated Import Paths

These paths still work but log a deprecation warning:

Deprecated Use instead
from limepkg_email.incoming_message.standard import StandardIncomingMessageProcessor from limepkg_email.incoming_message.processors import StandardIncomingMessageProcessor
from limepkg_email.incoming_message.incoming import IncomingMessage from limepkg_email.incoming_message.adapters import IncomingMessageInterface

Benefits of Migration

Your custom logic runs inside the import pipeline instead of before it. If pre_process fails, the import shows up as failed on the System Health page and can be retried from there, using the saved EML file. With the old task, a failure before import_message() only ended up in the task handler log.

Migration Example

Before (deprecated task):

from lime_task import task
from limepkg_email.incoming_message import import_message
from limepkg_email.incoming_message.standard import (
    StandardIncomingMessageProcessor,
)  # deprecated import path


class CustomProcessor(StandardIncomingMessageProcessor):
    def send_automatic_reply(self) -> bool:
        cc_addresses = [cc.address for cc in self.message.cc_receivers]
        return super().send_automatic_reply(blacklist=cc_addresses)


@task
def incoming(app, message, account_id):
    if should_skip(message):
        handle_special_case(app, message)
    else:
        import_message(app, message, account_id, CustomProcessor)

After (processor):

from limepkg_email.incoming_message.processors import (
    StandardIncomingMessageProcessor,
)


class CustomProcessor(StandardIncomingMessageProcessor):
    def pre_process(self) -> bool:
        if not should_skip(self.message):
            return True  # Continue import

        handle_special_case(self.app, self.message)
        return False  # Skip import

    def send_automatic_reply(self) -> bool:
        cc_addresses = [cc.address for cc in self.message.cc_receivers]
        return super().send_automatic_reply(blacklist=cc_addresses)