Development of Mailing Module for 1C-Bitrix
The built-in mailing module in 1C-Bitrix (subscribe) has existed for a long time, but in real-world projects it quickly hits its limitations: lack of flexible segmentation, no event triggers, difficult integration with external SMTP providers. When business needs outgrow the standard solution — you build a custom module.
Problems with the Standard subscribe Module
The standard module stores subscribers in the b_subscribe_subscr_addr table and email templates in b_subscribe_posting. The sending logic operates through CAgent agents, which run synchronously during HTTP requests. With a database of 10,000+ contacts, this leads to timeouts and missed messages.
Additionally: no built-in open tracking, no retry queue for SMTP errors, no support for transactional emails with dynamic content via API.
Custom Module Architecture
The module registers in the standard way via /bitrix/modules/vendor.mailer/install/index.php with the vendor_mailer class. The namespace is \Vendor\Mailer.
Key components:
-
SubscriberTable — ORM table
b_vendor_mailer_subscriber(id, email, name, status, groups, created_at, unsubscribed_at) -
CampaignTable —
b_vendor_mailer_campaign(id, name, subject, template_id, status, scheduled_at, sent_count, open_count) -
QueueTable —
b_vendor_mailer_queue(id, campaign_id, subscriber_id, status, attempts, last_error, sent_at) -
EventTable —
b_vendor_mailer_event(id, subscriber_id, type, payload, created_at) — tracking opens and clicks
Tables are created via \Bitrix\Main\ORM\Data\DataManager with full D7-ORM support.
Sending Queue
Synchronous sending via CAgent is inefficient. We use a queue instead:
// Enqueueing when campaign starts
public function enqueue(int $campaignId): void
{
$subscribers = SubscriberTable::getList([
'filter' => ['=STATUS' => 'active', '=GROUPS' => $this->campaign->getGroups()],
'select' => ['ID', 'EMAIL', 'NAME'],
]);
while ($row = $subscribers->fetch()) {
QueueTable::add([
'CAMPAIGN_ID' => $campaignId,
'SUBSCRIBER_ID' => $row['ID'],
'STATUS' => 'pending',
'ATTEMPTS' => 0,
]);
}
}
The queue processor is launched by an agent every 2 minutes, takes a batch of 100 records with pending status, sends them through the selected transport, and updates the status to sent or failed.
Transports: SMTP and API Providers
The transport abstraction is implemented via the MailTransportInterface:
interface MailTransportInterface
{
public function send(Message $message): SendResult;
}
Concrete implementations: SmtpTransport (via PHPMailer or native mail()), SendGridTransport (REST API v3), MailgunTransport, AmazonSesTransport. The transport is selected in the module settings (b_option, namespace vendor.mailer).
On send error, the status transitions to failed, and the attempts counter increments. When attempts >= 3, the record is marked as dead and is not processed again.
Open and Click Tracking
A transparent 1×1 pixel is inserted into the email:
<img src="https://site.ru/mailer/track/open/?uid=UNIQUE_TOKEN" width="1" height="1">
On GET request, the controller logs the event in b_vendor_mailer_event and returns a 1×1 GIF. Clicks are tracked via redirect: all links in the email are replaced with https://site.ru/mailer/track/click/?uid=TOKEN&url=ENCODED_URL.
Event handler:
EventTable::add([
'SUBSCRIBER_ID' => $subscriberId,
'CAMPAIGN_ID' => $campaignId,
'TYPE' => 'open', // or 'click'
'PAYLOAD' => json_encode(['url' => $url, 'ip' => $_SERVER['REMOTE_ADDR']]),
'CREATED_AT' => new \Bitrix\Main\Type\DateTime(),
]);
Subscriber Segmentation
Groups are stored in b_vendor_mailer_group, with subscriber relationships in b_vendor_mailer_subscriber_group. Segmentation supports conditions based on Bitrix user profile fields (b_user) via JOIN.
Dynamic segments are built through a filter builder in the administrative interface — the same approach as in Bitrix CRM: a set of conditions with AND/OR operators, which are translated into ORM filters when enqueueing.
Transactional Emails
For system emails (email confirmation, password reset, order notifications), the module provides an event API:
// Sending transactional email
\Vendor\Mailer\Transactional::send('order_confirmed', [
'USER_ID' => $userId,
'ORDER_ID' => $orderId,
'ORDER_SUM' => $orderSum,
'ITEMS' => $orderItems,
]);
Transactional email templates are stored in b_vendor_mailer_template and edited through the administrative section with support for variables in the format {{ORDER_ID}}.
Administrative Interface
The module adds sections to /bitrix/admin/:
-
vendor_mailer_subscribers.php— subscriber list and management -
vendor_mailer_campaigns.php— campaigns, status, statistics -
vendor_mailer_templates.php— template editor -
vendor_mailer_settings.php— transport settings, DKIM, limits
The interface is built on standard classes CAdminList, CAdminForm — fully compatible with Bitrix theme styling.
Unsubscribe and GDPR
An unsubscribe link in each email leads to https://site.ru/mailer/unsubscribe/?token=TOKEN. Upon clicking, the subscriber's status changes to unsubscribed, and the unsubscribed_at field is populated. Re-enqueueing for such records is blocked at the filter level.
For GDPR compliance: export subscriber data on request (GET /mailer/export-personal/?token=TOKEN), complete deletion via SubscriberTable::delete() with cascading deletion from all related tables.
Development Timeline
| Stage | Duration |
|---|---|
| Module structure, ORM tables, installer | 2 days |
| Queue and sending agents | 2 days |
| SMTP transport + 1 API provider | 1 day |
| Open and click tracking | 1 day |
| Segmentation, groups | 1 day |
| Transactional emails | 1 day |
| Administrative interface | 2 days |
| Unsubscribe, GDPR, testing | 1 day |
Total: 11 working days for basic version. Connecting additional API providers (Mailchimp, UniSender) — +1 day each.







