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.
- Create a new Python module in your solution (e.g.,
solution_pizza/message_processors.py). - Implement a class that extends
StandardIncomingMessageProcessor. - Override the
pre_processmethod to filter or route messages, and/or override individual processing methods to customize behavior. - Register your processor in
Application Configurationwith theconfig.limepkg_email.incoming_message_processorkey. - 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 linemessage.sender- Sender address and namemessage.to_receivers- List of To recipientsmessage.cc_receivers- List of CC recipientsmessage.html- HTML body contentmessage.plain_text- Plain text body contentmessage.message_id- Unique Message-ID headermessage.date- Message datemessage.attachments- List of attachmentsmessage.headers- Full email headersmessage.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¶
- Restart the task handler after every code change.
- Send an email with the word "pizza" in the subject and expect a history note to be created.
- 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_conversationfunction should create aConversationobject and return it. It's your responsibility to add any objects you create to the unit of work. You may use theself.uowproperty 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¶
- Make sure the new import flow is turned on.
- Use the processor class your task already passed to
import_message(), or create a new class that extendsStandardIncomingMessageProcessor. - If your task had logic before calling
import_message(), move it intopre_processon that class. ReturnTrueto import the email, orFalseif 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 timepre_processruns. - Read message data from
self.messageinstead of the webhook payload. If your task downloaded the email file withdownload_raw_message(), useself.message.eml_fileinstead. See Reading Message Data. - Update imports that use deprecated paths.
- In Application Config, remove
incoming_message_taskand setincoming_message_processorto 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. - Delete the old task.
- 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_processruns after the email has been downloaded and moved to the processed folder. ReturningFalsedoesn't leave the email in the inbox.- If
pre_processreturnsTrue, add any objects you create toself.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_processreturnsFalse, nothing else commitsself.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)