10 KiB
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.
Here’s 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.phpglobally.
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).