Documentation menu

Guides

Managing mailboxes, aliases and groups

The API that sends your email can also set up your company's mail: mailboxes, their alternate emails (aliases) and groups. This guide scripts the set-up of several domains and many mailboxes with the CLI or an SDK, in a way that can safely be run again.

Every change follows the console's rules, so a script can do exactly what its key's creator could do by hand. For a one-off import of a few users, the console's CSV upload does the same without a script. The endpoints are described in Mailboxes and Groups.

Before you start

  • A full-access key. It acts with the console privileges of the person who created it: View, Create, Update and Suspend users, Reset user passwords, View and Manage groups. Deleting mailboxes and groups needs a key created by a company owner. See API keys.
  • Verified domains. Mailboxes, alternate emails and groups can only use domains whose ownership the company has proven.
  • Room in the plan. The plan sets the number of users and the largest mailbox. Mailbox sizes above the plan's are lowered to it; a mailbox beyond the user limit is refused with 403 validation_error.

The examples read the key from TSMART_API_KEY:

Shell
npm install -g https://tsmart.ai/cli/tsmart-cli-1.1.0.tgz
export TSMART_API_KEY=ts_xxxxxxxx_…   # or: tsmart login

1. Add and verify the domains

Add each domain, publish the ownership TXT record that the answer shows, then verify. Once ownership is proven (status is no longer not_started), the domain is created on our mail server and can hold mailboxes. A company can have at most 10 domains waiting for ownership verification at a time, so add a longer list in rounds.

CLI
for d in acme.com acme.de acme.fr; do
  tsmart domains create "$d"        # prints the ownership record to publish
done

tsmart domains list                 # the ids
tsmart domains verify 6a0f3c1e-2b4d-4e8f-9a1b-3c5d7e9f1a2b

With an SDK, verify every domain that is still waiting:

Node.js
import { Tsmart } from "@tsmart/sdk";

const tsmart = new Tsmart();
const { data } = await tsmart.domains.list({ limit: 100 });
for (const d of data.data.filter((d) => d.status === "not_started")) {
  await tsmart.domains.verify(d.id);
}

Do not change the domains' MX records yet: incoming mail moves only when you switch MX, after the mailboxes exist (see step 5). Domains and DNS describes every record.

2. Create the mailboxes

Keep the users in a CSV file:

users.csv
email,first_name,last_name,quota_mb
jane@acme.com,Jane,Doe,2048
sam@acme.com,Sam,Lee,2048
anna@acme.de,Anna,Schmidt,4096

Each new mailbox gets a 20-character temporary password, returned only in the answer that creates it. The user must choose a new password at the first sign-in. The scripts below write the passwords to a file that only you can read; give each user theirs over a safe channel, then delete the file. Keep the passwords out of logs and CI output.

Node.js
// setup-mailboxes.mjs: node setup-mailboxes.mjs users.csv
import { appendFileSync, readFileSync } from "node:fs";
import { Tsmart } from "@tsmart/sdk";

const tsmart = new Tsmart(); // reads TSMART_API_KEY
const [, ...rows] = readFileSync(process.argv[2], "utf8").trim().split(/\r?\n/);

for (const row of rows) {
  const [email, first_name, last_name, quota_mb] = row.split(",");
  const { data, error } = await tsmart.mailboxes.create({ email, first_name, last_name, quota_mb: Number(quota_mb) });
  if (error?.name === "already_exists") {
    console.log("exists ", email); // created on an earlier run
  } else if (error) {
    console.error("failed ", email, error.name, error.message);
    if (error.statusCode === 403) break; // missing privilege, plan limit or inactive subscription: stop
  } else {
    appendFileSync("passwords.csv", `${data.email},${data.temporary_password}\n`, { mode: 0o600 });
    console.log("created", email);
  }
}
Python
# setup_mailboxes.py: python setup_mailboxes.py users.csv
import csv
import os
import sys

from tsmart import Tsmart, TsmartError

client = Tsmart()  # reads TSMART_API_KEY
fd = os.open("passwords.csv", os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600)
with open(sys.argv[1], newline="") as users, os.fdopen(fd, "a") as passwords:
    for row in csv.DictReader(users):
        try:
            box = client.mailboxes.create({
                "email": row["email"],
                "first_name": row["first_name"],
                "last_name": row["last_name"],
                "quota_mb": int(row["quota_mb"]),
            })
        except TsmartError as e:
            if e.name == "already_exists":
                print("exists ", row["email"])  # created on an earlier run
                continue
            print("failed ", row["email"], e.name, e.message, file=sys.stderr)
            if e.status_code == 403:
                break  # missing privilege, plan limit or inactive subscription: stop
            continue
        passwords.write("%s,%s\n" % (box["email"], box["temporary_password"]))
        print("created", box["email"])

The same with the CLI, in a shell loop. It prints the temporary password on standard output only:

CLI
umask 077                             # passwords.txt is readable by you only
tail -n +2 users.csv | while IFS=, read -r email first last quota; do
  out=$(tsmart mailboxes create "$email" --first-name "$first" --last-name "$last" --quota-mb "$quota") || continue
  echo "$email $(echo "$out" | sed -n 's/^temporary_password: *//p')" >> passwords.txt
done

Add org_unit_id (--org-unit-id) to place a mailbox in an organisational unit; it then takes that unit's storage and mail app policies at once. Without it, the mailbox goes to the root unit. A quota_mb (256 to 51200) above the plan's mailbox size is lowered to it; without one, the company's default size applies.

3. Add alternate emails

An alternate email delivers to a mailbox, and the mailbox may also send as it. It can be in any of the company's verified domains, so one mailbox can receive mail for the same name in several domains.

CLI
tsmart mailboxes alias-add jane@acme.com j.doe@acme.com
tsmart mailboxes alias-add jane@acme.com jane@acme.de
tsmart mailboxes aliases jane@acme.com
Node.js
const aliases = { "jane@acme.com": ["j.doe@acme.com", "jane@acme.de"], "sam@acme.com": ["s.lee@acme.com"] };
for (const [mailbox, list] of Object.entries(aliases)) {
  for (const address of list) {
    const { error } = await tsmart.mailboxes.aliases.create(mailbox, { address });
    if (error && error.name !== "already_exists") console.error(address, error.name, error.message);
  }
}

4. Create the groups

A group delivers each message to all its members: up to 200 mailboxes of the company or outside addresses. With the CLI, members can come from a file with one address per line:

CLI
tsmart groups create team@acme.com --name "Team" --member jane@acme.com,sam@acme.com
tsmart groups create all@acme.com --name "All staff" --members-file staff.txt

tsmart groups update team@acme.com --add-member anna@acme.de --remove-member sam@acme.com
tsmart groups get team@acme.com

members in an update is always the complete list. To make a script safe to run again, create the group and, when it already exists, send the full list:

Python
def ensure_group(address, name, members):
    try:
        return client.groups.create({"address": address, "name": name, "members": members})
    except TsmartError as e:
        if e.name != "already_exists":
            raise
    return client.groups.update(address, {"name": name, "members": members})  # bring it in line

ensure_group("team@acme.com", "Team", ["jane@acme.com", "anna@acme.de"])

already_exists also answers when the address belongs to a mailbox or an alternate email; the update then answers 404 not_found. A group without members discards its mail.

Check the result

Mailboxes and groups are listed by address, and a page is continued with the last address as the cursor. domain narrows a list to one domain.

CLI
tsmart mailboxes list --domain acme.com --limit 100
tsmart groups list --domain acme.com
Node.js
let after;
do {
  const { data, error } = await tsmart.mailboxes.list({ domain: "acme.com", limit: 100, after });
  if (error) throw new Error(error.message);
  for (const m of data.data) console.log(m.email, m.quota_mb, m.aliases.join(" "));
  after = data.has_more ? data.data.at(-1).email : undefined;
} while (after);

5. Switch incoming mail

When the mailboxes, alternate emails and groups are in place, point each domain's MX record to mail.tsmart.ai (see MX). Mail for the domain then arrives in the new mailboxes. Do it on the day you switch: changing MX moves all incoming mail of the domain.

Day-to-day changes

CLI
tsmart mailboxes update jane@acme.com --name "Jane Smith" --quota-mb 4096
tsmart mailboxes update sam@acme.com --active off      # suspend: mail is still received, no sign-in
tsmart mailboxes update sam@acme.com --active on       # restore
tsmart mailboxes reset-password jane@acme.com --yes    # a new temporary password, shown once
tsmart mailboxes alias-remove jane@acme.com jane@acme.de
tsmart groups delete team@acme.com --yes               # company owners only
tsmart mailboxes delete sam@acme.com --yes             # with all its mail; company owners only

Errors to expect

StatusNameMeaning and what to do
409already_existsThe address is already a mailbox, a group or an alternate email. On a re-run, skip it.
422validation_errorAn address outside the company's verified domains, a part before @ that is not allowed, a size out of range, or a bad member list. Fix the input.
403validation_errorThe plan's user limit is reached. Stop, and change the plan in the console.
403invalid_permissionThe key's creator lacks the privilege, or the right for that organisational unit. Use a key of an admin who has it.
403account_inactiveThe company's subscription is not active.
404not_foundThe mailbox or group does not exist in the company's domains.
429rate_limit_exceededMore than 10 requests per second. The SDKs and the CLI wait and retry for you.
502mail_server_errorThe mail server refused the change; the message carries its error codes. Read the object again, then retry or contact support.