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

  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 classmethod 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

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):
        """Don't send automatic replies to CC recipients."""
        cc_addresses = [r.address for r in self.message.cc_receivers]
        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.

Migration Patterns

Old pattern (deprecated) New pattern
config.limepkg_email.incoming_message_task: my.tasks.incoming config.limepkg_email.incoming_message_processor: my.module.MyProcessor
Custom Celery task with import_message() call Override pre_process and/or individual steps
Filter + call import_message() pre_process returns True
Filter + call import_message() with custom processor Override pre_process and individual steps
Handle message yourself (skip import_message) pre_process returns False

Benefits of Migration

  • Full state tracking: Messages are tracked through the import pipeline with visibility into current state
  • Automatic retry: Failed imports are automatically retried with exponential backoff
  • Recovery support: Failed messages can be recovered and reprocessed
  • No webhook data dependency: Retries work from saved EML files, no need to preserve webhook payloads

Migration Example

Before (deprecated task):

@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):

class CustomProcessor(StandardIncomingMessageProcessor):
    def pre_process(self) -> bool:
        if should_skip(self.message):
            handle_special_case(self.app, self.message)
            return False  # Skip import
        return True  # Continue import

    # Override individual methods as needed
    def send_automatic_reply(self):
        # Custom logic here
        super().send_automatic_reply(blacklist=blacklist)