Skip to content

Tenant API v1 — Email

Base path: /api/tenant/v1

Middleware: auth:tenant-api, tenant.user, not.suspended, verified, module:email, plus Spatie permission middleware / policies.

Route parameters resolve account, folder, label, message, template, and signature models by uuid. Accounts, folders, labels, messages, and signatures are personal (authenticated user only via mailbox ownership). Templates may be shared (is_shared) for workspace-wide apply.

Personal mailbox traffic does not use EmailConfigResolver or Settings → Mail. OAuth is not available in v1.x (IMAP/SMTP credentials / app passwords only). Users may connect multiple accounts; compose requires account_uuid (SPA sends the mailbox chosen in From).

Accounts

MethodPathPermission
GET/email/accountsemail.view
POST/email/accountsemail.accounts.manage
GET/email/accounts/{uuid}email.view (own)
PUT/email/accounts/{uuid}email.accounts.manage
DELETE/email/accounts/{uuid}email.accounts.manage (disconnect + purge local cache)
POST/email/accounts/testemail.accounts.manage
POST/email/accounts/{uuid}/syncemail.accounts.manage
POST/email/accounts/{uuid}/defaultemail.accounts.manage

POST /email/accounts and POST /email/accounts/test accept IMAP + SMTP host/port/encryption/username/password, email_address, optional from_name, is_default, and optional sync_interval_minutes (5 | 10 | 15 | 30 | 60, default 5). Passwords are stored with the Eloquent encrypted cast. Manual POST …/sync always queues a sync regardless of interval.

Folders

MethodPathPermission
GET/email/accounts/{uuid}/foldersemail.view (own account)
GET/email/folders/{uuid}email.view (own)

Returns synced folders (uuid, name, type, remote_path, unread/total counts).

Labels

EloSync-only tags per mailbox (not synced to IMAP). A message may have many labels while remaining in one folder.

MethodPathPermission
GET/email/accounts/{uuid}/labelsemail.view (own account)
POST/email/accounts/{uuid}/labelsemail.update
GET/email/labels/{uuid}email.view (own)
PUT/email/labels/{uuid}email.update
DELETE/email/labels/{uuid}email.update
GET/email/labels/{uuid}/messagesemail.view
PUT/email/messages/{uuid}/labelsemail.update

Create/update body: { "name": "Follow up", "color": "#2563eb", "sort_order": 1 } (color optional #RRGGBB; name unique per mailbox).

Sync message labels: { "label_uuids": ["…"] } (replace set; empty array clears — field must be present). Label UUIDs must belong to the same mailbox as the message. Message list/show responses include labels when loaded.

Messages

MethodPathPermission
GET/email/folders/{uuid}/messagesemail.view
GET/email/accounts/{uuid}/messages/search?q=email.view
GET/email/messages/{uuid}email.view
POST/email/messages/bulkemail.update or email.delete (by action)
POST/email/messages/composeemail.create
PUT/email/messages/{uuid}email.update
PUT/email/messages/{uuid}/labelsemail.update
POST/email/messages/{uuid}/sendemail.update
DELETE/email/messages/{uuid}email.delete
POST/email/messages/{uuid}/linkemail.update
DELETE/email/messages/{uuid}/linkemail.update

Bulk body (max 25 UUIDs; capped for synchronous IMAP timeout safety):

json
{
  "message_uuids": ["…"],
  "action": "delete | move | flags | labels",
  "folder_uuid": "…",
  "is_read": true,
  "label_uuids": ["…"],
  "label_mode": "add | remove"
}

folder_uuid required for move; is_read for flags; label_uuids + label_mode for labels. Response data: { "processed": n, "failed": [{ "uuid", "message" }] }. All-failed batches return 422.

Compose body requires account_uuid, optional to/cc/bcc arrays, subject, body_html / body_text, and optional in_reply_to / thread_key for replies. Creates a draft; call send to queue SendEmailMessageJob on email-sync.

Update supports is_read, is_starred, folder_uuid (IMAP move to another folder on the same account), and draft field edits.

DELETE /email/messages/{uuid} moves the message to the account Trash folder when present and the message is not already in Trash; otherwise permanently deletes (IMAP expunge + local row). Success message indicates which path ran. If IMAP reports the remote UID is already gone, the API still deletes the local row and returns a permanent-delete success. Transient IMAP failures (auth/network) return an error and leave the local row.

GET /email/messages/{uuid} returns body_html and body_text. When body_html is null, the service may re-fetch the body from IMAP once so nested multipart HTML is available in the reading pane (empty string after a successful fetch means no HTML part).

SPA reading pane renders body_html in a sandboxed iframe (allow-popups only) after DOMPurify sanitization (scripts stripped; styles kept).

Link body: { "linkable_type": "lead\|contact\|company\|opportunity", "linkable_id": 1 }. Requires email.update on the message and authorization to view the target record (policy). There is no Email SPA linking UI in v1.

Templates

MethodPathPermission
GET/POST/email/templatesview / email.templates.manage
GET/PUT/DELETE/email/templates/{uuid}view / manage (update/delete: creator or workspace owner)
POST/email/templates/{uuid}/renderemail.view (own, shared, or owner)

Create/update accept name, category, subject, body_html, optional variables, is_active, and is_shared (boolean; default true on create).

List query params:

  • for_compose=1 — active templates the actor may apply (own or shared). Workspace owner does not receive others’ private templates in this mode.
  • Without for_compose — management list: own or shared; workspace owner sees all templates.
  • Optional filters: search, category, is_active, is_shared.

Resource fields include uuid, is_shared, user { id, name, email }, can_edit, can_delete. Apply/render always uses uuid (names are not unique across users).

Render body: { "variables": { "name": "Ada" } }{ subject, body_html } with replaced. Inactive templates return 403 on render for viewers who cannot update them (creators / workspace owner may still render while editing).

Signatures

MethodPathPermission
GET/POST/email/signaturesview / email.signatures.manage
GET/PUT/DELETE/email/signatures/{uuid}view / manage

Ops notes

  • Queue workers: dedicated email-sync daemon (see deployment); keep notifications on emails,default
  • Scheduler: email:sync every minute — dispatches jobs only for mailboxes whose sync_interval_minutes (default 5) has elapsed
  • Production sync requires PHP ext-imap
  • Platform transactional mail remains under Settings → Mail / Email Logs

Official documentation for the EloSync SaaS Platform.