Search by

mkerstner / write-assistant

mkerstner

Write, rewrite and proofread e-mail and summarise received messages with a local LLM served by Ollama. All model calls are made server-side to an allow-listed endpoint, so no mail content is sent to third parties.

Package info

github.com/mkerstner/roundcube-plugin-write-assistant

Type:roundcube-plugin

pkg:composer/mkerstner/write-assistant

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.1 2026-11-02 14:38 UTC

This package is auto-updated.

Last update: 2026-11-02 14:40:32 UTC


README

Write, rewrite and proofread e-mail, and summarise received messages, with a local LLM served by Ollama. All model calls are made server-side by PHP to an allow-listed endpoint; the browser never talks to Ollama and no mail content is sent to OpenAI or any other third party.

Built for CPU-only servers: answers stream in as they are generated, summaries never block opening a message and give way to compose requests, and requests nobody waits for any more are cancelled.

Features

  • Write an e-mail from instructions in any language, in one of 36 output languages, with a style (assertive, casual, enthusiastic, funny, informational, professional, urgent, witty), a length (short / medium / long) and a creativity setting (mapped to the model temperature). Optionally uses the message being replied to as context and suggests a subject (only filled in when the subject field is empty).
  • Rewrite the selected text or your own part of the draft in another style, length or language.
  • Correct spelling, grammar and punctuation without changing the wording.
  • Context menu on selected text (right-click): Correct, Improve writing, Shorten, Expand, Change tone ▸, Translate ▸, and the full dialog. The result is shown first and replaces the selection on click. Right-click without a selection, or Ctrl+right-click, keeps the browser's menu with its spelling suggestions. Quick actions are configurable (write_assistant_quick_actions).
  • Works with the plain-text editor and the HTML (TinyMCE) editor. Signature, reply intro, quoted text and forwarded message are left alone; the result is shown first and only enters the message when you click Insert/Replace (undo works). In HTML drafts, Rewrite/Correct return plain paragraphs; the dialog warns when that would drop formatting.
  • Summaries in the user's Roundcube language above an opened message (automatic after a short delay, or on click), and on hover over a subject in the message list (by default only summaries that already exist, so hovering never starts the model). Cached per message.
  • Admin-customisable prompts, styles, lengths, languages and models.
  • Per-user switches in Settings > Preferences ("Composing Messages", "Displaying Messages"), enforceable via dont_override.

Requirements

  • Roundcube 1.6 or later (developed against 1.7.5), PHP 8.1+ with cURL.
  • Elastic or a skin that extends it (e.g. Aurora) for the toolbar icon; everything else is skin-independent.
  • Ollama on the Roundcube server itself (the default; see below), with a model pulled, e.g. ollama pull gemma4:12b. About 10 GB of free RAM for a 12 B model on CPU.

Installing Ollama on the Roundcube server

By default the plugin expects Ollama on the same machine as Roundcube, at http://localhost:11434, and only allows loopback addresses. Ollama itself listens on 127.0.0.1 by default, so it is not reachable from the network and mail content never leaves the server. Keep it that way: do not set OLLAMA_HOST.

On Linux (see https://docs.ollama.com/linux for other setups):

curl -fsSL https://ollama.com/install.sh | sh   # installs the "ollama" service
ollama pull gemma4:12b
sudo systemctl status ollama

Recommended service settings for a CPU-only server (sudo systemctl edit ollama, then sudo systemctl restart ollama):

[Service]
Environment="OLLAMA_NUM_PARALLEL=1"
Environment="OLLAMA_MAX_LOADED_MODELS=2"

If Roundcube runs in a container, localhost is the container, not the host: either run Ollama in the same container network (and point write_assistant_url at it), or use the host's address, e.g. http://host.docker.internal:11434, and add that name to write_assistant_allowed_hosts.

Installation

A. Via the Roundcube plugin installer (recommended)

Run from the Roundcube root:

composer require mkerstner/write-assistant

The roundcube/plugin-installer (declared as a dependency) then:

  • installs the plugin into plugins/write_assistant/ (Composer package names use a hyphen, Roundcube plugin names an underscore),
  • copies config.inc.php.dist to config.inc.php if no config exists yet,
  • adds write_assistant to the plugins list in config/config.inc.php (when that file is writable and Composer runs on the CLI),
  • refuses to install on Roundcube older than min-version (1.6.0).

B. Manual installation

  1. Put this directory at plugins/write_assistant/.
  2. Optional: copy config.inc.php.dist to config.inc.php and change what you need (models, prompts, ...). Every option has a documented default in config.inc.php.dist, which matches a same-host Ollama with gemma4:12b.
  3. Add 'write_assistant' to $config['plugins'] in config/config.inc.php.

Where settings go

Put plugin settings into plugins/write_assistant/config.inc.php. Roundcube loads that file after config/config.inc.php, so its values win. The plugin installer creates it as a full copy of config.inc.php.dist, which means a write_assistant_* option set only in config/config.inc.php is ignored. Either change the option in the plugin's config.inc.php, or delete that file and keep the settings in config/config.inc.php (Roundcube then also reads config/write_assistant.inc.php). The health check below shows which file each model setting comes from.

Check the setup

php plugins/write_assistant/bin/check.php --generate

It verifies the endpoint allow-list, that Ollama answers, that every configured model is installed and that temp_dir is writable, and with --generate times a tiny request per model. Run it with the command-line PHP your Roundcube uses: on shared hosting, php in the shell is often an older version or the CGI variant (the script says so), and the right binary is something like php8.3 or /usr/local/bin/php83.

To use an Ollama on another host instead, set write_assistant_url and add that host to write_assistant_allowed_hosts. Ollama has no authentication: never expose it beyond the Roundcube server without a firewall, or put it behind an authenticating reverse proxy and set write_assistant_headers.

Ollama on a CPU-only server

  • Keep write_assistant_num_ctx the same for all models: Ollama reloads a model whenever the context size changes.
  • A smaller summary model is several times faster, e.g. write_assistant_models = ['compose' => 'gemma4:12b', 'summary' => 'gemma4:e4b-it-qat']. Let Ollama keep both in memory (OLLAMA_MAX_LOADED_MODELS=2, see above) and keep a long write_assistant_keep_alive (loading takes 25–40 s on CPU).
  • Leave OLLAMA_NUM_PARALLEL at 1: requests are served one after another, and the plugin makes summaries give way while someone is composing (via flag files in Roundcube's temp_dir, so this works per Roundcube host).

Streaming behind a reverse proxy

Generated text and summaries are sent as newline-delimited JSON. The plugin disables PHP output buffering and compression for these responses and sends X-Accel-Buffering: no (nginx). With other buffering proxies, or if text only appears at the end, set write_assistant_stream to false.

The plugin sends a heartbeat line every 5 s while the model is still reading the prompt, which keeps proxy read timeouts from firing and lets PHP notice when the browser has gone away (it then cancels the model run). Raise write_assistant_timeout and PHP-FPM's request_terminate_timeout if answers take longer than 3 minutes. Keep nginx's fastcgi_ignore_client_abort off (the default), so closed browser connections reach PHP.

Troubleshooting

The messages users see are deliberately generic. The technical reason is logged to Roundcube's logs/errors.log (lines starting with write_assistant:), and php plugins/write_assistant/bin/check.php, run on the Roundcube server, tests the same connection the plugin uses.

  • "The assistant is not reachable right now": PHP cannot connect to write_assistant_url. localhost means the machine (or container) Roundcube runs on, not your desktop. Ollama listens on 127.0.0.1 only by default: to serve another host, start it with OLLAMA_HOST=0.0.0.0:11434, restrict access with a firewall, point write_assistant_url at it and add its name or IP to write_assistant_allowed_hosts. From a Docker container, the Docker host is usually host.docker.internal.
  • "The configured AI model is not available on the server": run ollama pull <model> on the Ollama host, or fix write_assistant_models.
  • "The assistant is not configured correctly": the URL's host is not in write_assistant_allowed_hosts.

Privacy and security

  • Requests go only to hosts in write_assistant_allowed_hosts; redirects are never followed and HTTP proxies are never used, even if Roundcube's http_client config or the environment defines one.
  • Ollama "cloud" models (gemma4:cloud, …-cloud) are refused: Ollama would forward their requests, and the mail content, to ollama.com.
  • Endpoints are protected by Roundcube's request token (CSRF).
  • Mail content is passed to the model as tagged data, separate from the admin's prompt, and output is always rendered as text. Prompt injection from incoming mail cannot be ruled out with small models, but the model has no tools or actions and its output is only displayed or suggested.
  • Per-user rate limit (write_assistant_rate_limit); logs never contain mail content (write_assistant_debug logs only timings and token counts).

Tests

PHPUnit tests live in tests/ and run like Roundcube's core plugin tests, from a Roundcube checkout with its test setup (test database configured in tests/config/; the test bootstrap recreates that database):

vendor/bin/phpunit -c tests/phpunit.xml plugins/write_assistant/tests

tests/cli_smoke.php runs live checks against a model, including a summary of a prompt-injection e-mail; useful to compare candidate models:

php plugins/write_assistant/tests/cli_smoke.php vendor/autoload.php gemma4:e4b-it-qat

License

Apache License, Version 2.0. See LICENSE.