Files
toolLooper/plan.md

10 KiB
Raw Permalink Blame History

You're on the right track! You want to create a Roundcube plugin that allows users to enhance or rewrite the email body using an LLM, by sending the current message content to a backend API (your LLM service), then replacing the textarea content with the response.

Heres a complete working example of such a plugin, called llm_compose_helper. It adds a button to the compose screen, sends the current message text to a configured URL via AJAX, and replaces the message body with the LLM-generated result.


Goal

  • Add a "Rewrite with AI" button in the compose window.
  • On click: open a popup asking the user for a rewrite prompt/instructions.
  • Submit both the current message body and the user prompt to the configured LLM API endpoint.
  • Replace the <textarea> content with the returned text.

📁 Plugin Structure

plugins/
  llm_compose_helper/
    llm_compose_helper.php     <-- Main plugin class
    config.inc.php             <-- Configuration file
    localization/en_US.inc     <-- Language labels
    js/llm_compose_helper.js   <-- Client-side JavaScript

1. llm_compose_helper.php Plugin Class

<?php

/**
 * LLM Compose Helper Plugin
 *
 * Allows rewriting the message body using an external LLM service
 *
 * @version 1.0
 * @author You
 * @license GPL
 */
class llm_compose_helper extends rcube_plugin
{
    public $task = 'mail';

    function init()
    {
        // Load configuration
        $this->load_config();

        // Load localization texts (available in JS if second param = true)
        $this->add_texts('localization/', true);

        // Only run in compose view
        $rcmail = rcmail::get_instance();
        if ($rcmail->action == 'compose') {
            // Include JS and CSS
            $this->include_script('js/llm_compose_helper.js');

            // Register UI placeholder handler
            $this->register_handler('plugin.llm_button', array($this, 'llm_button'));
        }
    }

    /**
     * Generate the button HTML to be inserted into the toolbar
     */
    function llm_button()
    {
        $rcmail = rcmail::get_instance();
        return html::tag('a', array(
            'href'    => '#',
            'id'      => 'llm_rewrite_button',
            'class'   => 'button',
            'title'   => $this->gettext('rewrite_with_llm'),
            'onclick' => 'return false;'
        ), $this->gettext('rewrite_label'));
    }
}

2. config.inc.php Plugin Configuration

<?php

// URL of your LLM API endpoint
$config['llm_api_url'] = 'https://your-llm-api.example.com/rewrite';

// Optional: headers (e.g., auth tokens)
$config['llm_api_headers'] = array(
    'Authorization: Bearer your-secret-token',
    'Content-Type: application/json'
);

💡 You can later override this in config/config.inc.php globally.


3. localization/en_US.inc

<?php

$labels = array(
    'rewrite_with_llm' => 'Rewrite message with AI',
    'rewrite_label'    => 'AI Rewrite',
    'enter_prompt'     => 'Enter rewrite prompt',
    'rewrite_submit'   => 'Rewrite',
    'rewrite_cancel'   => 'Cancel',
);

$messages = array(
    'prompt_placeholder' => 'e.g., Make it shorter and more formal',
);

Add other language files like fr_FR.inc, de_DE.inc, etc., if needed.


4. js/llm_compose_helper.js Client-Side Logic

// Wait for Roundcube to initialize
rcmail.addEventListener('init', function() {
    const button = document.getElementById('llm_rewrite_button');
    if (!button) return;

    // Insert button into compose toolbar
    const toolbar = rcmail.gui_objects.toolbar;
    if (toolbar) {
        toolbar.appendChild(button);
    }

    // Attach click event
    $(button).click(function(e) {
        e.preventDefault();

        const textarea = rcmail.env.html_editor ? rcmail.editor : document.getElementById('composebody');
        let messageText;

        // Get current message body
        if (rcmail.env.html_editor && rcmail.editor && rcmail.editor.getData) {
            messageText = rcmail.editor.getData(); // CKEditor
        } else {
            messageText = $('#composebody').val();
        }

        if (!messageText || messageText.trim() === '') {
            alert(rcmail.gettext('non_empty', 'llm_compose_helper'));
            return;
        }

        // Build prompt dialog content
        var promptId = 'llm_prompt_input_' + Date.now();
        var dialogHtml = '<div style="padding:8px 0">' +
            '<label for="' + promptId + '">' + rcmail.gettext('enter_prompt', 'llm_compose_helper') + '</label>' +
            '<textarea id="' + promptId + '" style="width:100%;height:120px;box-sizing:border-box;margin-top:6px" placeholder="' + (rcmail.gettext('prompt_placeholder', 'llm_compose_helper') || '') + '"></textarea>' +
            '</div>';

        var buttons = [
            {
                text: rcmail.gettext('rewrite_submit', 'llm_compose_helper'),
                classes: 'mainaction',
                click: function(e, ref) {
                    var promptValue = document.getElementById(promptId).value || '';

                    // Show loading
                    rcmail.set_busy(true, 'loading');

                    // Send to LLM API with message and prompt
                    rcmail.http_post('plugin.llm_rewrite', {
                        message: messageText,
                        prompt: promptValue
                    }, function() {
                        rcmail.set_busy(false);
                    });

                    if (ref && ref.hide) ref.hide();
                }
            },
            {
                text: rcmail.gettext('rewrite_cancel', 'llm_compose_helper'),
                click: function(e, ref) { if (ref && ref.hide) ref.hide(); }
            }
        ];

        // Open Roundcube dialog
        rcmail.show_popup_dialog(dialogHtml, rcmail.gettext('rewrite_with_llm', 'llm_compose_helper'), buttons, {modal: true, width: 520});
    });
});

// Handle response from server
rcmail.addEventListener('plugin.llm_rewrite_response', function(response) {
    if (response.status === 'success' && response.text) {
        const newText = response.text;

        if (rcmail.env.html_editor && rcmail.editor && rcmail.editor.setData) {
            rcmail.editor.setData(newText); // For CKEditor
        } else {
            $('#composebody').val(newText);
        }

        rcmail.showMessage(rcmail.gettext('rewrite_success', 'llm_compose_helper'), 'confirmation');
    } else {
        var errorMsg = response && response.message ? String(response.message) : rcmail.gettext('rewrite_error', 'llm_compose_helper');
        rcmail.showMessage(errorMsg, 'error');
    }
});

5. Extend llm_compose_helper.php Add Server-Side Action

Update the llm_compose_helper.php file to register the AJAX action and handle the request:

function init()
{
    $this->load_config();
    $this->add_texts('localization/', true);

    $rcmail = rcmail::get_instance();
    if ($rcmail->action == 'compose') {
        $this->include_script('js/llm_compose_helper.js');
        $this->register_handler('plugin.llm_button', array($this, 'llm_button'));

        // Register custom action for AJAX
        $this->register_action('plugin.llm_rewrite', array($this, 'action_handler'));
    }
}

function action_handler()
{
    $rcmail = rcmail::get_instance();

    // Get input
    $message = rcube_utils::get_input_value('message', rcube_utils::INPUT_POST);
    $prompt  = rcube_utils::get_input_value('prompt', rcube_utils::INPUT_POST);
    if (empty($message)) {
        $rcmail->output->command('plugin.llm_rewrite_response', [
            'status' => 'error',
            'message' => 'No message provided'
        ]);
        return;
    }

    // Get config
    $api_url = $rcmail->config->get('llm_api_url');
    if (!$api_url) {
        $rcmail->output->command('plugin.llm_rewrite_response', [
            'status' => 'error',
            'message' => 'LLM API URL not configured'
        ]);
        return;
    }

    $headers = $rcmail->config->get('llm_api_headers', ['Content-Type: application/json']);

    // Prepare request
    $data = json_encode(['text' => $message, 'prompt' => $prompt]);

    // Use file_get_contents or cURL
    $options = [
        'http' => [
            'header'  => $headers,
            'method'  => 'POST',
            'content' => $data,
            'timeout' => 30
        ],
        'ssl' => [
            'verify_peer'       => true,
            'verify_peer_name'  => true,
            'cafile'            => '/etc/ssl/certs/ca-certificates.crt', // Adjust as needed
        ]
    ];

    $context = stream_context_create($options);
    $result = file_get_contents($api_url, false, $context);

    if ($result === false) {
        $rcmail->output->command('plugin.llm_rewrite_response', [
            'status' => 'error',
            'message' => 'Request failed'
        ]);
        return;
    }

    $response = json_decode($result, true);
    $rewritten = isset($response['text']) && $response['text'] !== null ? $response['text'] : ($response['message'] ?? $result);

    // Return success
    $rcmail->output->command('plugin.llm_rewrite_response', [
        'status' => 'success',
        'text'   => $rewritten
    ]);
}

6. Example LLM API Response Format (Expected)

Your external LLM endpoint should accept POST JSON:

{ "text": "Hello, I am writing to ask about...", "prompt": "Make it shorter and more formal" }

And return:

{ "text": "Hi, I hope you're doing well. I'm reaching out to inquire about..." }

Ensure CORS is allowed if hosted separately.


7. Optional: Add More Labels

In localization/en_US.inc, add:

$messages = array(
    'rewrite_success' => 'Message rewritten successfully!',
    'rewrite_error'   => 'Failed to rewrite message.',
    'non_empty'       => 'Please write something before using AI rewrite.'
);

8. Enable the Plugin

In config/config.inc.php:

$config['plugins'] = array(
    // ... other plugins
    'llm_compose_helper'
);

Final Result

When composing or replying:

  • A new button labeled "AI Rewrite" appears in the toolbar.
  • Clicking it opens a dialog asking for a rewrite prompt.
  • Submitting sends the current message and the prompt to your LLM API.
  • The response replaces the message body.
  • Works with plain text and HTML (via CKEditor if enabled).