Echomail Robots

March 10, 2026 · View on GitHub

Draft — This document was generated by AI and may not have been reviewed for accuracy.

Echomail Robots

Echomail robots watch echo areas for matching messages and automatically process them. The framework is designed to be generic and extensible: any number of robot rules can be configured, each pointing at an echo area with a processor that knows how to handle that type of message.


How It Works

A robot rule (echomail_robots table) links an echo area to a processor. When the runner executes, it:

  1. Queries the echo area for messages newer than its last-processed cursor (last_processed_echomail_id)
  2. Optionally filters by a case-insensitive subject substring (subject_pattern)
  3. Passes each matching message to the configured processor
  4. Advances the cursor and updates run statistics

Robots are stateless between runs — progress is tracked entirely in the database row.


Running Robots

* * * * * php /path/to/binkterm-php/scripts/echomail_robots.php --quiet

Manually from the command line

# Run all enabled robots
php scripts/echomail_robots.php

# Run a specific robot by ID
php scripts/echomail_robots.php --robot-id=3

# Preview without writing anything
php scripts/echomail_robots.php --dry-run

# Suppress output
php scripts/echomail_robots.php --quiet

Output for each robot:

[OK]    Robot #1 (FSX_DAT iBBS): examined=42 processed=38
[ERROR] Robot #2 (Bad Robot): examined=0 processed=0 error=Unknown processor type: fake_type

Via the admin UI

Navigate to Admin → Area Management → Echomail Robots. Click the ▶ Run Now button on any robot row to execute it immediately and see a summary toast.


Database Schema

echomail_robots

ColumnTypeDescription
idSERIAL PK
nameVARCHAR(100)Human-readable label
echoarea_idINTEGER FKEcho area to watch
subject_patternVARCHAR(255)Substring match on subject (case-insensitive, ILIKE). Leave null to process all messages.
processor_typeVARCHAR(100)Matches a registered processor's getProcessorType()
processor_configJSONBOptional processor-specific configuration
enabledBOOLEANInactive robots are skipped by the runner
last_processed_echomail_idINTEGERCursor — next run starts at id > this
last_run_atTIMESTAMPTZTimestamp of last execution
messages_examinedINTEGERRunning total of messages seen
messages_processedINTEGERRunning total of messages handled
last_errorTEXTLast exception message, if any

Source Files

FilePurpose
src/Robots/MessageProcessorInterface.phpInterface all processors must implement
src/Robots/EchomailRobotRunner.phpFetches messages, dispatches to processors, updates cursors
src/Robots/Processors/IbbsLastCallProcessor.phpBuilt-in processor for iBBS last-call announcements
scripts/echomail_robots.phpCLI entry point

Writing a New Processor

Create a class in src/Robots/Processors/ that implements MessageProcessorInterface:

<?php

namespace BinktermPHP\Robots\Processors;

use BinktermPHP\Robots\MessageProcessorInterface;

class MyProcessor implements MessageProcessorInterface
{
    private \PDO $db;

    public function __construct(\PDO $db)
    {
        $this->db = $db;
    }

    public static function getProcessorType(): string
    {
        return 'my_processor';  // stored in echomail_robots.processor_type
    }

    public static function getDisplayName(): string
    {
        return 'My Processor';
    }

    public static function getDescription(): string
    {
        return 'Does something useful with matching messages.';
    }

    public function processMessage(array $message, array $robotConfig): bool
    {
        // $message contains echomail table columns:
        //   id, subject, from_name, to_name, message_text, echoarea_id,
        //   date, origin, etc.
        //
        // $robotConfig is the decoded processor_config JSONB from the robot rule.
        //
        // Return true if the message was handled, false to skip/ignore.

        $text = $message['message_text'] ?? '';
        if (empty($text)) {
            return false;
        }

        // ... do work ...

        return true;
    }
}

Then register it in EchomailRobotRunner::PROCESSORS:

private const PROCESSORS = [
    'ibbslastcall_rot47' => IbbsLastCallProcessor::class,
    'my_processor'       => \BinktermPHP\Robots\Processors\MyProcessor::class,
];

The new processor will immediately appear in the admin UI's processor-type dropdown.

The $message array

The array passed to processMessage() is a raw row from the echomail table. Commonly used columns:

ColumnDescription
idMessage ID
subjectMessage subject
from_nameAuthor name
to_nameRecipient name (All for broadcast messages)
message_textFull message body
echoarea_idEcho area ID
dateMessage date string
originOrigin line

Processor config

The processor_config JSONB column allows per-robot configuration for a processor. Pass anything you need — the decoded array is provided as $robotConfig to every processMessage() call. Currently the admin UI does not expose a config editor; values can be set directly in the database or added to the modal in a future update.


Built-in Processors

ibbslastcall_rot47 — iBBS Last Call (ROT47)

Handles BBS node announcement messages posted to the FSX_DAT echo area by iBBS software. These messages have the subject ibbslastcall-data and a ROT47-encoded body.

Recommended robot rule settings:

FieldValue
Echo AreaFSX_DAT (or whichever area carries these messages)
Subject Patternibbslastcall-data
Processor Typeibbslastcall_rot47

Message body format (after ROT47 decode, newline-separated):

Line 0  >>> BEGIN marker
Line 1  Sysop name
Line 2  BBS name
Line 3  Date (MM/DD/YY or YY/MM/DD)
Line 4  Time
Line 5  Location
Line 6  Operating system
Line 7  telnet-host:port
Line 8  >>> END marker

On a successful parse the processor upserts a row in bbs_directory. If the BBS name already exists (case-insensitive), the sysop, location, OS, and telnet address are updated and last_seen is refreshed. Manual entries (source = 'manual') are updated in place but their source flag is preserved.

ROT47 maps every printable ASCII character in the range 33–126 by rotating 47 positions: chr((ord(c) - 33 + 47) % 94 + 33). It is its own inverse — applying it twice returns the original text.


Future: Netmail Robots

The interface is designed to work for netmail as well. When needed:

  1. Create a netmail_robots table (same shape as echomail_robots, substituting to_address VARCHAR for echoarea_id)
  2. Add src/Robots/NetmailRobotRunner.php — identical logic, queries the netmail table
  3. Add scripts/netmail_robots.php CLI entry point

All existing MessageProcessorInterface implementations will work without modification; they receive the same $message array shape regardless of source table.