Soft-deleted users deprecated

Soft-deleted users

A soft-deleted user is a leaver: the account is gone from the organization's point of view, but its data survives a retention period and an administrator can bring it back. The state sits between a disabled user (mailenabled=false, a plain lock that changes nothing but the ability to log in) and the irreversible deletion.

What the state does

A soft-deleted user

  • cannot log in on any path, and existing sessions end with the next request;
  • is hidden from the global address book and from auto-completion;
  • cannot be invited to appointments and answers no free/busy; an invitation addressed to the user is handled like one to an external address;
  • is hidden from group member lists, while the memberships themselves are kept and survive a group update through a client that did not see the member;
  • withholds everything it shared from everybody else, see below;
  • stops the guests it invited and the share links it created: the links no longer resolve, the guests can no longer log in;
  • keeps mail, files, calendar and contacts, and keeps its login name, mail address and aliases reserved, so no other account can take them over meanwhile;
  • counts as a disabled user in the reports.

The shares the user granted are frozen, without exception: shared calendars, address books and task folders, the personal files and the files shared one by one, and shared mail folders (a deputy's access included) are hidden from every other user and cannot be opened, no matter what the stored permissions say. A deputy can neither read the mailbox nor send mail in the user's name, and the grant is absent from the deputy's listing meanwhile. The permissions themselves are not touched, so a restore brings the shares back as they were. Public folders the user created are no shares and stay as they are. Nothing is removed, nothing is moved; a hand-over of the data happens after a restore, or not at all.

The mailenabled flag is not touched. A user that was disabled before being soft-deleted stays disabled after being restored.

Provisioning

The state is set and lifted through the provisioning APIs; the context administrator, guests and shared accounts cannot be soft-deleted.

API Soft-delete Restore List soft-deleted users
RMI OXUserInterface.softDelete(ctx, user, auth), softDelete(ctx, user, destUser, auth) OXUserInterface.restore(ctx, user, auth) OXUserInterface.listSoftDeleted(ctx, auth)
gRPC / HTTP DeleteUsers with mode: SOFT, POST /prov/v1/contexts/{cid}/users:delete RestoreUsers, POST /prov/v1/contexts/{cid}/users:restore ListUserData with soft_deleted: true, GET /prov/v1/contexts/{cid}/users?soft_deleted=true
SOAP softDelete / softDeleteMultiple restore / restoreMultiple listSoftDeleted
SCIM DELETE /Users/{id} with com.openexchange.scim.deleteMode=softDelete, or DELETE /Users/{id}?deleteMode=softDelete not exposed, see SCIM provisioning not exposed

The user data carries the time of the soft-deletion (softDeleted over RMI, soft_deleted over gRPC and SOAP), so a listing can tell soft-deleted users apart. The value is read-only. The ordinary listings keep including soft-deleted users; the dedicated listing returns only them.

curl -u oxadmin:secret -H 'Content-Type: application/json' \
  -d '{"users":[{"id":3}],"mode":"SOFT"}' https://provisioning.example.com/prov/v1/contexts/1/users:delete
curl -u oxadmin:secret -H 'Content-Type: application/json' \
  -d '{"users":[{"id":3}]}' https://provisioning.example.com/prov/v1/contexts/1/users:restore

A soft-deleted user can still be deleted for good with the ordinary delete operation, and its attributes can still be changed. Over SCIM, DELETE /Users/{id}?deleteMode=delete does the same: it removes the account at once, soft-deleted or not, for a request under the right to be forgotten that must not wait for the retention period.

Who inherits the shared data

Deleting a user hands the data shared with others to somebody else, which deleteuser --reassign names for an ordinary deletion. A soft-deleted user is deleted only once the retention period has passed, months later and without a caller present, so the destination is named up front and remembered until it is used.

Destination
softdeleteuser --reassign <id> the named user takes the shared data
softdeleteuser --no-reassign the shared data is dropped
neither the context administrator takes it, as before

The same choice is available on every provisioning API that soft-deletes: destUser on the RMI overload, reassign on the SOAP requests, and dest_user on DeleteUsers with mode: SOFT, which was accepted but ignored before.

softdeleteuser -c 1 -i 3 -A oxadmin -P secret --reassign 5
curl -u oxadmin:secret -H 'Content-Type: application/json'   -d '{"users":[{"id":3}],"mode":"SOFT","destUser":5}' https://provisioning.example.com/prov/v1/contexts/1/users:delete

The destination is checked when it is named - it has to exist, and it may be neither a guest, nor the user being soft-deleted, nor a soft-deleted user itself - and checked again before it is used. A destination that stopped qualifying in the meantime, because it was deleted or became a leaver itself, falls back to the context administrator rather than failing the deletion, which would leave the user pending for good. It is remembered as the user attribute softDelete/reassignTo and can be read back with the other attributes.

Restoring a user forgets the destination along with the soft-deleted state. There is no separate call to change it: soft-delete the user again, naming a different destination.

Retention

Once the retention period has passed, a soft-deleted user is deleted for good by the clean-up job com.openexchange.admin.softdelete.SoftDeleteRetentionExecution, which runs hourly on one node of the cluster (see Database Cleanup Jobs). The deletion is the same as an explicit delete through the provisioning API: files of a user with an own filestore are moved to the filestore of whoever inherits the shared data, and the provisioning plugins are called.

The retention period is configured through com.openexchange.user.softDelete.retentionDays, config-cascade aware down to the context. The default is 30 days; 0 keeps soft-deleted users until an administrator deletes or restores them.

com.openexchange.user.softDelete.retentionDays=30

A purge that fails, because a filestore is unreachable or a provisioning plugin refuses, leaves the user soft-deleted and is retried with the next run. Every attempt is counted, so an alert can be raised on the failures:

appsuite_provisioning_softdelete_purges_total{result="success|failure"}

The user itself carries the last failure as the attribute purgeFailed in the namespace softDelete, holding the time and the error, until the purge succeeds or the user is restored. It comes with the user's data over every provisioning API (userAttributes over RMI, user_attributes over gRPC); the listing of soft-deleted users itself carries only the identifiers and the time of the soft-deletion. The trail described below records the failure as well.

Audit trail

Every step of the lifecycle is written to the logger com.openexchange.provisioning.userLifecycleAuditTrail at INFO, one line per user and event: the soft-deletion, the restore, the deletion for good (by an administrator or by the retention job, then naming the time the user had been soft-deleted), and a failed purge. Each line names the acting administrator and, where the request came in over SCIM, the origin recorded by the SCIM endpoint:

soft-deleted user 7 in context 1 by oxadmin via scim basic oxadmin in context 1
deleted user 7 in context 1 by soft-delete-retention, soft-deleted since 2026-09-09T11:00:00Z
failed to purge user 7 in context 1 by soft-delete-retention, soft-deleted since 2026-09-09T11:00:00Z: ...

Route the logger to an appender of its own, append-only where the trail is to be kept, and give that appender a pattern with %lmdc, so the line keeps the tracking identifier and the client address; the appender's timestamp is the time of the event:

<logger name="com.openexchange.provisioning.userLifecycleAuditTrail" level="INFO" additivity="false">
    <appender-ref ref="USER_LIFECYCLE_AUDIT" />
</logger>

What clients see

The HTTP API exposes the state to clients, so that a user interface can tell a leaver apart from a merely disabled account:

  • The user module renders soft_deleted (column 628), the time of the soft-deletion in milliseconds since the epoch (UTC, not shifted by the user's time zone); the attribute is absent for every other user. Soft-deleted users are absent from users?action=all and users?action=search, which list the address book; a users?action=get with the identifier still answers, with the attribute set.
  • A login of a soft-deleted user is refused with AUTHORIZATION-0004 ("The account is disabled and pending deletion") instead of the AUTHORIZATION-0001 of a disabled user.
  • A session of a user that has been soft-deleted meanwhile expires with SES-0203 like the session of a disabled user; the expiration reason the middleware records for it is usersoftdeleted instead of userdisabled.
  • On an appointment, a soft-deleted user resolves to an external attendee (the internal reference is dropped, the appointment data itself is not changed), carrying the read-only extended parameter X-OX-SOFT-DELETED whose value is the time of the soft-deletion. The marker is applied on read only: it is never stored and is left out of exported iCal data. The organizer of an event keeps its internal reference, so it stays resolvable through soft_deleted and the change-organizer operation still works.

What the state does not cover

Background work the user has set up keeps running: a pending data export completes, and a mail scheduled for later is still sent. The mail backend is not told about the state, so mail addressed to the user is still delivered to the mailbox and kept there. Synchronization clients get no tombstone for the user's address book entry; it disappears with the next full listing.