This document covers every changes an Ops needs to be aware of when running Twake Mail backend mail server.
The following procedures are to take as it, and Linagora, nor its contributors, can not be responsible for any damages generated by following the below procedures.
Before performing these operations, you should ensure to have the skills to conduct the operations, and you should read other software documentation. Do not follow this guide blindly!
Note: this section is in progress. It will be updated during all the development process until the release.
Date: 12/03/2026
Concerned product: Distributed TMail, Postgres TMail
Add the optional read_only field to the labels table. When set to true, a label cannot be updated or deleted by the user via JMAP (Label/set). Read-only labels can still be managed by administrators through the WebAdmin API.
Labels created before this migration will have NULL for this column, which is treated as false (not read-only) by the application.
IMPORTANT: These commands must be executed BEFORE deploying the new version.
To add this column, run the following CQL command:
ALTER TABLE labels ADD read_only boolean;To add this column, run the following SQL command:
ALTER TABLE labels ADD COLUMN read_only BOOLEAN;Date: 12/03/2026
Concerned product: Distributed TMail, Postgres TMail
Add can_upgrade and is_paying fields to the domains table to support domain-level SaaS account configuration.
These fields enable domain-wide SaaS plan defaults that apply to all users of the domain when no user-level
override is set.
IMPORTANT: These commands must be executed BEFORE deploying the new version.
To add these columns, run the following CQL commands:
ALTER TABLE domains ADD can_upgrade boolean;
ALTER TABLE domains ADD is_paying boolean;To add these columns, run the following SQL commands:
ALTER TABLE domains ADD COLUMN can_upgrade BOOLEAN;
ALTER TABLE domains ADD COLUMN is_paying BOOLEAN;Date: 04/03/2026
Issue: #2214
Concerned product: Distributed TMail, Postgres TMail
Add the optional activated field to the domains table to allow setting up rate limits on a domain before it's being activated.
IMPORTANT: These commands must be executed BEFORE deploying the new version.
To add this column, run the following CQL command:
ALTER TABLE domains ADD activated boolean;Note that existing domains prior to this addition without the activated field setup will be considered as unactivated. You need then to set manually existing domains to activated = true.
To add this column, run the following CQL command:
ALTER TABLE domains ADD COLUMN activated BOOLEAN;Note that existing domains prior to this addition without the activated field setup will be considered as unactivated. You need then to set manually existing domains to activated = true.
Date: 04/02/2026
Issue: #2115
Listener's name change: The listener LlmMailPrioritizationClassifierListener has been renamed to LlmMailBackendClassifierListener.
Before:
<listener>
<class>com.linagora.tmail.james.jmap.llm.LlmMailPrioritizationClassifierListener</class>
</listener>After:
<listener>
<class>com.linagora.tmail.james.jmap.llm.LlmMailBackendClassifierListener</class>
</listener>After upgrading LlmMailPrioritizationClassifierListener to LlmMailClassifierListener, we must delete the old RabbitMQ queue to avoid resource leaks and prevent the old listener from processing messages.
Queue to Delete: mailboxEvent-workQueue-com.linagora.tmail.listener.rag.LlmMailPrioritizationClassifierListener$LlmMailPrioritizationClassifierGroup
Date: 28/01/2026
Issue: #2114
Concerned product: Distributed TMail, Postgres TMail
Add the optional description field to the labels table to allow users to provide clear descriptions for their labels. This enables better frontend synchronization.
IMPORTANT: These commands must be executed BEFORE deploying the new version.
To add this column, run the following CQL command:
ALTER TABLE labels ADD description text;To add this column, run the following SQL command:
ALTER TABLE labels ADD COLUMN description VARCHAR;Date: 15/10/2025
Issue: #1927
Concerned product: Distributed TMail
Add the following columns to the domains table to store per-domain mail rate limiting:
mails_sent_per_minute(BIGINT)mails_sent_per_hour(BIGINT)mails_sent_per_day(BIGINT)mails_received_per_minute(BIGINT)mails_received_per_hour(BIGINT)mails_received_per_day(BIGINT)
To add these columns, run the following CQL commands:
ALTER TABLE tmail_keyspace.domains ADD mails_sent_per_minute bigint;
ALTER TABLE tmail_keyspace.domains ADD mails_sent_per_hour bigint;
ALTER TABLE tmail_keyspace.domains ADD mails_sent_per_day bigint;
ALTER TABLE tmail_keyspace.domains ADD mails_received_per_minute bigint;
ALTER TABLE tmail_keyspace.domains ADD mails_received_per_hour bigint;
ALTER TABLE tmail_keyspace.domains ADD mails_received_per_day bigint;
Date: 15/10/2025
Issue: #1927
Concerned product: Postgres TMail
Add the following columns to the domains table to store per-domain mail rate limiting:
mails_sent_per_minute(BIGINT)mails_sent_per_hour(BIGINT)mails_sent_per_day(BIGINT)mails_received_per_minute(BIGINT)mails_received_per_hour(BIGINT)mails_received_per_day(BIGINT)
To add these columns, run the following SQL commands:
ALTER TABLE tmail_schema.domains ADD COLUMN mails_sent_per_minute BIGINT;
ALTER TABLE tmail_schema.domains ADD COLUMN mails_sent_per_hour BIGINT;
ALTER TABLE tmail_schema.domains ADD COLUMN mails_sent_per_day BIGINT;
ALTER TABLE tmail_schema.domains ADD COLUMN mails_received_per_minute BIGINT;
ALTER TABLE tmail_schema.domains ADD COLUMN mails_received_per_hour BIGINT;
ALTER TABLE tmail_schema.domains ADD COLUMN mails_received_per_day BIGINT;
Date: 01/09/2025
Issue: #1831
Concerned product: Distributed TMail
Add can_upgrade, is_payingcolumns to the user table to store saas plan information.
To add these columns, you need to run the following CQL commands:
ALTER TABLE tmail_keyspace.user ADD can_upgrade boolean;
ALTER TABLE tmail_keyspace.user ADD is_paying boolean;
Date: 08/09/2025
Issue: #1888
Concerned product: Distributed TMail
Add the following columns to the user table to store per-user mail rate limiting:
mails_sent_per_minute(BIGINT)mails_sent_per_hour(BIGINT)mails_sent_per_day(BIGINT)mails_received_per_minute(BIGINT)mails_received_per_hour(BIGINT)mails_received_per_day(BIGINT)
To add these columns, run the following CQL commands:
ALTER TABLE tmail_keyspace.user ADD mails_sent_per_minute bigint;
ALTER TABLE tmail_keyspace.user ADD mails_sent_per_hour bigint;
ALTER TABLE tmail_keyspace.user ADD mails_sent_per_day bigint;
ALTER TABLE tmail_keyspace.user ADD mails_received_per_minute bigint;
ALTER TABLE tmail_keyspace.user ADD mails_received_per_hour bigint;
ALTER TABLE tmail_keyspace.user ADD mails_received_per_day bigint;
Date: 01/09/2025
Issue: #1831
Concerned product: Postgres TMail
Add can_upgrade, is_payingcolumns to the user table to store saas plan information.
To add these columns, you need to run the following SQL commands:
ALTER TABLE tmail_schema.users ADD COLUMN is_paying BOOLEAN;
ALTER TABLE tmail_schema.users ADD COLUMN can_upgrade VARCHAR;
Date: 08/09/2025
Issue: #1888
Concerned product: Postgres TMail
Add the following columns to the users table to store per-user mail rate limiting:
mails_sent_per_minute(BIGINT)mails_sent_per_hour(BIGINT)mails_sent_per_day(BIGINT)mails_received_per_minute(BIGINT)mails_received_per_hour(BIGINT)mails_received_per_day(BIGINT)
To add these columns, run the following SQL commands:
ALTER TABLE tmail_schema.users ADD COLUMN mails_sent_per_minute BIGINT;
ALTER TABLE tmail_schema.users ADD COLUMN mails_sent_per_hour BIGINT;
ALTER TABLE tmail_schema.users ADD COLUMN mails_sent_per_day BIGINT;
ALTER TABLE tmail_schema.users ADD COLUMN mails_received_per_minute BIGINT;
ALTER TABLE tmail_schema.users ADD COLUMN mails_received_per_hour BIGINT;
ALTER TABLE tmail_schema.users ADD COLUMN mails_received_per_day BIGINT;
Date: 16/07/2025
Issue: #1831
Concerned product: Distributed TMail
Add settings, settings_state, and rate_limiting_plan_id columns to the user table to store user-bound settings and rate limiting plan information.
To add these columns, you need to run the following CQL commands:
ALTER TABLE tmail_keyspace.user ADD settings map<text, text>;
ALTER TABLE tmail_keyspace.user ADD settings_state uuid;
ALTER TABLE tmail_keyspace.user ADD rate_limiting_plan_id uuid;
The rate_limit_plan_user table and settings can be dropped now:
DROP TABLE tmail_keyspace.rate_limit_plan_user;
DROP TABLE tmail_keyspace.settings;
Date: 04/08/2025
Issue: #1840
Concerned product: Distributed TMail
If you are running TMail in a SaaS deployment with the extension DistributedSaaSModule enabled in extensions.properties, you need to add the saas_plan column to the Cassandra user table:
ALTER TABLE tmail_keyspace.user ADD saas_plan text;
Otherwise, you can skip this step.
Date: 17/07/2025
Issue: #1835
Concerned product: Postgres TMail
Add settings, settings_state, and rate_limiting_plan_id columns to the users table to store user-bound settings and rate limiting plan information.
To add these columns, you need to run the following SQL commands:
ALTER TABLE tmail_schema.users ADD COLUMN settings HSTORE;
ALTER TABLE tmail_schema.users ADD COLUMN settings_state UUID;
ALTER TABLE tmail_schema.users ADD COLUMN rate_limiting_plan_id UUID;The rate_limit_plan_user table and jmap_settings table can be dropped now:
DROP TABLE tmail_schema.rate_limit_plan_user;
DROP TABLE tmail_schema.jmap_settings;Date: 05/08/2025
Issue: #1840
Concerned product: Postgres TMail
If you are running TMail in a SaaS deployment with the extension PostgresSaaSModule enabled in extensions.properties, you need to add the saas_plan column to the Postgres users table:
ALTER TABLE tmail_schema.users ADD COLUMN saas_plan VARCHAR;
Add new field addressBookId to the user_contact index.
PUT /user_contact/_mapping
{
"properties": {
"addressBookId": {
"type": "keyword"
}
}
}
Date: 01/10/2024
Issue: #1204
Optional to adapt as the effection is minor.
Minimum ngram (minimum characters to autocomplete) now defaults to 2 (previously was 3).
To adapt the change, we need to migrate contact indices to a new version with the new indices settings.
- Step 1: create a v2 index
PUT /domain_contact_v2
{
"settings": {
"index": {
"max_ngram_diff": "27",
"number_of_shards": "5",
"number_of_replicas": "1",
"analysis": {
"filter": {
"ngram_filter": {
"type": "ngram",
"min_gram": "2",
"max_gram": "29"
},
"edge_ngram_filter": {
"type": "edge_ngram",
"min_gram": "2",
"max_gram": "29"
},
"preserved_ascii_folding_filter": {
"type": "asciifolding",
"preserve_original": "true"
}
},
"analyzer": {
"email_ngram_filter_analyzer": {
"filter": [
"ngram_filter",
"lowercase"
],
"type": "custom",
"tokenizer": "uax_url_email"
},
"rebuilt_keyword": {
"filter": [
"lowercase"
],
"type": "custom",
"tokenizer": "keyword"
},
"name_edge_ngram_filter_analyzer": {
"filter": [
"edge_ngram_filter",
"lowercase",
"preserved_ascii_folding_filter"
],
"type": "custom",
"tokenizer": "standard"
}
}
}
}
},
"mappings": {
"properties": {
"contactId": {
"type": "keyword"
},
"domain": {
"type": "keyword"
},
"email": {
"type": "text",
"analyzer": "email_ngram_filter_analyzer",
"search_analyzer": "rebuilt_keyword"
},
"firstname": {
"type": "text",
"analyzer": "name_edge_ngram_filter_analyzer",
"search_analyzer": "standard"
},
"surname": {
"type": "text",
"analyzer": "name_edge_ngram_filter_analyzer",
"search_analyzer": "standard"
}
}
}
}
Notes: We may need to change number_of_shards and number_of_replicas values if needed (have a look at opensearch.properties).
- Step 2: Expose the new index under the write alias
POST /_aliases
{
"actions": [
{
"remove": {
"index": "domain_contact",
"alias": "domain_contact_write_alias"
}
},
{
"add": {
"index": "domain_contact_v2",
"alias": "domain_contact_write_alias",
"is_write_index": true
}
}
]
}
- Step 3: Reindex v2 from v1 index
POST /_reindex?slices=auto
{
"source": {
"index": "domain_contact"
},
"dest": {
"index": "domain_contact_v2"
}
}
- Step 4: Expose the new index under the read alias
POST /_aliases
{
"actions": [
{
"remove": {
"index": "domain_contact",
"alias": "domain_contact_read_alias"
}
},
{
"add": {
"index": "domain_contact_v2",
"alias": "domain_contact_read_alias"
}
}
]
}
- Step 5: Delete v1 index
DELETE /domain_contact
- Step 1: create a v2 index
PUT /user_contact_v2
{
"settings": {
"index": {
"max_ngram_diff": "27",
"number_of_shards": "5",
"analysis": {
"filter": {
"ngram_filter": {
"type": "ngram",
"min_gram": "2",
"max_gram": "29"
},
"edge_ngram_filter": {
"type": "edge_ngram",
"min_gram": "2",
"max_gram": "29"
},
"preserved_ascii_folding_filter": {
"type": "asciifolding",
"preserve_original": "true"
}
},
"analyzer": {
"email_ngram_filter_analyzer": {
"filter": [
"ngram_filter",
"lowercase"
],
"type": "custom",
"tokenizer": "uax_url_email"
},
"rebuilt_keyword": {
"filter": [
"lowercase"
],
"type": "custom",
"tokenizer": "keyword"
},
"name_edge_ngram_filter_analyzer": {
"filter": [
"edge_ngram_filter",
"lowercase",
"preserved_ascii_folding_filter"
],
"type": "custom",
"tokenizer": "standard"
}
}
},
"number_of_replicas": "1"
}
},
"mappings": {
"properties": {
"accountId": {
"type": "keyword"
},
"contactId": {
"type": "keyword"
},
"email": {
"type": "text",
"analyzer": "email_ngram_filter_analyzer",
"search_analyzer": "rebuilt_keyword"
},
"firstname": {
"type": "text",
"analyzer": "name_edge_ngram_filter_analyzer",
"search_analyzer": "standard"
},
"surname": {
"type": "text",
"analyzer": "name_edge_ngram_filter_analyzer",
"search_analyzer": "standard"
}
}
}
}
Notes: We may need to change number_of_shards and number_of_replicas values if needed (have a look at opensearch.properties).
- Step 2: Expose the new index under the write alias
POST /_aliases
{
"actions": [
{
"remove": {
"index": "user_contact",
"alias": "user_contact_write_alias"
}
},
{
"add": {
"index": "user_contact_v2",
"alias": "user_contact_write_alias",
"is_write_index": true
}
}
]
}
- Step 3: Reindex v2 from v1 index
POST /_reindex?slices=auto
{
"source": {
"index": "user_contact"
},
"dest": {
"index": "user_contact_v2"
}
}
- Step 4: Expose the new index under the read alias
POST /_aliases
{
"actions": [
{
"remove": {
"index": "user_contact",
"alias": "user_contact_read_alias"
}
},
{
"add": {
"index": "user_contact_v2",
"alias": "user_contact_read_alias"
}
}
]
}
- Step 5: Delete v1 index
DELETE /user_contact
Change list:
Date 14/12/2022
Ticket: #534
Concerned product: Distributed Twake Mail, Distributed ES6 Twake Mail
We changed an analyzer setting for user and domain autocomplete indexes, therefore we need to reindex these two indexes
to make the new analyzer applies for existing documents.
We need to perform reindexing with aliases for zero downtime as we normally did.
New indexes settings + mapping (pay attention to change some change-me values - could get those values by getting the old indexes' settings):
- new
usercontact index:
{
"settings": {
"number_of_shards": change-me,
"number_of_replicas": change-me,
"index.write.wait_for_active_shards": change-me,
"index": {
"max_ngram_diff": change-me
},
"analysis": {
"analyzer": {
"email_ngram_filter_analyzer": {
"tokenizer": "uax_url_email",
"filter": ["ngram_filter", "lowercase"]
},
"name_edge_ngram_filter_analyzer": {
"tokenizer": "standard",
"filter": ["edge_ngram_filter", "lowercase", "preserved_ascii_folding_filter"]
},
"rebuilt_keyword": {
"tokenizer": "keyword",
"filter": ["lowercase"]
}
},
"filter": {
"ngram_filter": {
"type": "ngram",
"min_gram": change-me,
"max_gram": change-me
},
"edge_ngram_filter": {
"type": "edge_ngram",
"min_gram": change-me,
"max_gram": change-me
},
"preserved_ascii_folding_filter": {
"type": "asciifolding",
"preserve_original": true
}
}
}
},
"mappings": {
"properties": {
"accountId": {
"type": "keyword"
},
"contactId": {
"type": "keyword"
},
"email": {
"type": "text",
"analyzer": "email_ngram_filter_analyzer",
"search_analyzer": "rebuilt_keyword"
},
"firstname": {
"type": "text",
"analyzer": "name_edge_ngram_filter_analyzer",
"search_analyzer": "standard"
},
"surname": {
"type": "text",
"analyzer": "name_edge_ngram_filter_analyzer",
"search_analyzer": "standard"
}
}
}
}
- new
domaincontact index:
{
"settings": {
"number_of_shards": change-me,
"number_of_replicas": change-me,
"index.write.wait_for_active_shards": change-me,
"index": {
"max_ngram_diff": change-me
},
"analysis": {
"analyzer": {
"email_ngram_filter_analyzer": {
"tokenizer": "uax_url_email",
"filter": ["ngram_filter", "lowercase"]
},
"name_edge_ngram_filter_analyzer": {
"tokenizer": "standard",
"filter": ["edge_ngram_filter", "lowercase", "preserved_ascii_folding_filter"]
},
"rebuilt_keyword": {
"tokenizer": "keyword",
"filter": ["lowercase"]
}
},
"filter": {
"ngram_filter": {
"type": "ngram",
"min_gram": change-me,
"max_gram": change-me
},
"edge_ngram_filter": {
"type": "edge_ngram",
"min_gram": change-me,
"max_gram": change-me
},
"preserved_ascii_folding_filter": {
"type": "asciifolding",
"preserve_original": true
}
}
}
},
"mappings": {
"properties": {
"domain": {
"type": "keyword"
},
"contactId": {
"type": "keyword"
},
"email": {
"type": "text",
"analyzer": "email_ngram_filter_analyzer",
"search_analyzer": "rebuilt_keyword"
},
"firstname": {
"type": "text",
"analyzer": "name_edge_ngram_filter_analyzer",
"search_analyzer": "standard"
},
"surname": {
"type": "text",
"analyzer": "name_edge_ngram_filter_analyzer",
"search_analyzer": "standard"
}
}
}
}
- new
usercontact index:
{
"settings": {
"number_of_shards": change-me,
"number_of_replicas": change-me,
"index.write.wait_for_active_shards": change-me,
"analysis": {
"analyzer": {
"email_ngram_filter_analyzer": {
"tokenizer": "uax_url_email",
"filter": ["ngram_filter", "lowercase"]
},
"name_edge_ngram_filter_analyzer": {
"tokenizer": "standard",
"filter": ["edge_ngram_filter", "lowercase", "preserved_ascii_folding_filter"]
},
"rebuilt_keyword": {
"tokenizer": "keyword",
"filter": ["lowercase"]
}
},
"filter": {
"ngram_filter": {
"type": "ngram",
"min_gram": change-me,
"max_gram": change-me
},
"edge_ngram_filter": {
"type": "edge_ngram",
"min_gram": change-me,
"max_gram": change-me
},
"preserved_ascii_folding_filter": {
"type": "asciifolding",
"preserve_original": true
}
}
}
},
"mappings": {
"properties": {
"accountId": {
"type": "keyword"
},
"contactId": {
"type": "keyword"
},
"email": {
"type": "text",
"analyzer": "email_ngram_filter_analyzer",
"search_analyzer": "rebuilt_keyword"
},
"firstname": {
"type": "text",
"analyzer": "name_edge_ngram_filter_analyzer",
"search_analyzer": "standard"
},
"surname": {
"type": "text",
"analyzer": "name_edge_ngram_filter_analyzer",
"search_analyzer": "standard"
}
}
}
}
- new
domaincontact index:
{
"settings": {
"number_of_shards": change-me,
"number_of_replicas": change-me,
"index.write.wait_for_active_shards": change-me,
"analysis": {
"analyzer": {
"email_ngram_filter_analyzer": {
"tokenizer": "uax_url_email",
"filter": ["ngram_filter", "lowercase"]
},
"name_edge_ngram_filter_analyzer": {
"tokenizer": "standard",
"filter": ["edge_ngram_filter", "lowercase", "preserved_ascii_folding_filter"]
},
"rebuilt_keyword": {
"tokenizer": "keyword",
"filter": ["lowercase"]
}
},
"filter": {
"ngram_filter": {
"type": "ngram",
"min_gram": change-me,
"max_gram": change-me
},
"edge_ngram_filter": {
"type": "edge_ngram",
"min_gram": change-me,
"max_gram": change-me
},
"preserved_ascii_folding_filter": {
"type": "asciifolding",
"preserve_original": true
}
}
}
},
"mappings": {
"properties": {
"domain": {
"type": "keyword"
},
"contactId": {
"type": "keyword"
},
"email": {
"type": "text",
"analyzer": "email_ngram_filter_analyzer",
"search_analyzer": "rebuilt_keyword"
},
"firstname": {
"type": "text",
"analyzer": "name_edge_ngram_filter_analyzer",
"search_analyzer": "standard"
},
"surname": {
"type": "text",
"analyzer": "name_edge_ngram_filter_analyzer",
"search_analyzer": "standard"
}
}
}
}
Change list:
Date 04/10/2022
JIRA: https://issues.apache.org/jira/browse/JAMES-3827
Concerned product: Distributed Twake Mail, Distributed ES6 Twake Mail
If you did not finish reindexing data as part of OpenSearch Migration, you can skip this instruction and just reindex data.
Otherwise, please add these fields to the mailbox mapping manually:
curl -X PUT \
http://OSip:OSport/{$mailboxIndexName}/_mapping \
-H 'Content-Type: application/json' \
-d '{
"properties": {
"from": {
"properties": {
"domain": {
"type": "text",
"analyzer": "simple",
"search_analyzer": "keyword"
}
}
},
"to": {
"properties": {
"domain": {
"type": "text",
"analyzer": "simple",
"search_analyzer": "keyword"
}
}
},
"cc": {
"properties": {
"domain": {
"type": "text",
"analyzer": "simple",
"search_analyzer": "keyword"
}
}
},
"bcc": {
"properties": {
"domain": {
"type": "text",
"analyzer": "simple",
"search_analyzer": "keyword"
}
}
}
}
}'