Lazy Expunge

The lazy expunge plugin provides a “second-chance” to recover messages that would otherwise be deleted from a mailbox by user action.

It does this by moving the message to a defined location (either a mailbox, or a namespace – see below for further details) when a user deletes the message from a mailbox.

This behavior is useful for a variety of reasons:

  1. Protect against misconfigured clients (e.g. POP3 client that deletes all messages)

  2. Protect against accidental deletion (user error)

  3. Archiving

Generally, lazy-expunge is configured so that the expunged mails are not counted in the user’s quota. Unless being used for archiving, autoexpunge should be used to prune the mailbox to control storage usage.

Configuration

There are two plugin configuration options:

lazy_expunge

The mailbox/namespace to move messages to when expunged

lazy_expunge_only_last_instance

If true, only move to expunged storage if this is the last copy of the message in the user’s account

Storage Locations

mailbox

New in version v2.2.24.

Messages that are expunged are moved to a single mailbox.

This is the simplest configuration. The mailbox is created automatically.

You probably also want to hide it with an ACL from the user, if recovery is only expected to be an action performed by an admin/operator.

To move to a mailbox, do NOT add a trailing delimiter to the lazy_expunge argument.

Example configuration:

namespace inbox {
  mailbox .EXPUNGED {
    autoexpunge = 7days
    autoexpunge_max_mails = 100000
  }
}

mail_plugins = $mail_plugins lazy_expunge acl
plugin {
  # Move messages to an .EXPUNGED mailbox
  lazy_expunge = .EXPUNGED

  # Define ACL so that user cannot list the .EXPUNGED mailbox
  acl = vfile:/etc/dovecot/dovecot.acl

  # Expunged messages most likely don't want to be included in quota:
  quota_rule = .EXPUNGED:ignore
}

Where /etc/dovecot/dovecot.acl contains:

.EXPUNGED owner rwstipekxa

You could also leave the permissions empty if you don’t want to allow clients to access it at all.

namespace

Deprecated since version v2.3.0.

Expunged messages are moved to mailbox(es) within a defined namespace

When a message is expunged from mailbox <name>, it’s moved to a mailbox <name> in the expunge namespace. When an entire mailbox <name> is deleted, it’s moved to this namespace as <name>. If the mailbox already exists in the expunge namespace, the contents are merged.

To move to a namespace, you MUST add a trailing delimiter to the lazy_expunge argument. Example: if the namespace delimiter is /, and you want to move to the .EXPUNGED namespace, then the lazy_expunge option should be set to .EXPUNGED/.

Example configuration:

# Default namespace
namespace {
  prefix =
  separator = /
  inbox = yes
}

# Namespace for lazy_expunge plugin
namespace {
  prefix = .EXPUNGED/
  hidden = yes
  list = no
  separator = /
  location = maildir:~/Maildir/expunged
}

mail_plugins = $mail_plugins lazy_expunge
plugin {
  # Move expunged messages into the .EXPUNGED namespace
  lazy_expunge = .EXPUNGED/
}
mdbox

With mdbox, use different MAILBOXDIRs (so copying between namespaces works quickly within the same storage), but otherwise exactly the same paths (INDEX, control):

# Default namespace
namespace {
  prefix =
  inbox = yes
  location = mdbox:~/mdbox:INDEX=/var/index/%d/%n
  separator = /
}

# lazy_expunge namespace(s)
namespace {
  prefix = .EXPUNGED/
  hidden = yes
  list = no
  separator = /
  subscriptions = no

  location = mdbox:~/mdbox:INDEX=/var/index/%d/%n:MAILBOXDIR=expunged

  # If mailbox_list_index=yes is enabled, it needs a separate index file
  # (v2.2.28+):
  #location = mdbox:~/mdbox:INDEX=/var/index/%d/%n:MAILBOXDIR=expunged:LISTINDEX=expunged.list.index
}

Copy only the last instance

If a mail has multiple copies within a user account, each copy is normally moved to the lazy expunge storage when it’s expunged.

Example: this may happen when moving a message to Trash, as clients can issue IMAP COPY command to copy the message to Trash before expunging the message from the original mailbox. Deleting later from Trash would result in two copies of the same message in the lazy expunge storage.

With v2.2+ you can set lazy_expunge_only_last_instance = yes to copy only the last instance to the expunge storage. This ensures that only a single copy of a message will appear in the expunge storage.

This setting works with the following mailbox formats:

  • Maildir (with maildir_copy_with_hardlinks = yes, which is the default)

  • sdbox

  • mdbox

  • obox with fs-dictmap

Quota

Generally, it is desired that messages in expunge storage are NOT counted towards user quota, as the messages seen by the user will not match-up with the size of the quota otherwise (especially if expunge storage is hidden from users via ACL).

Example to exclude expunge storage from the quota:

plugin {
  quota = count:User quota
  quota_rule = *:storage=1GB
  # Exclude .EXPUNGED mailbox from the quota
  quota_rule2 = .EXPUNGED:ignore
}

See Quota.

Cleaning up

doveadm

Doveadm can be used to manually clean expunge storage.

Example to delete all messages in .EXPUNGED namespace older than one day:

doveadm expunge mailbox '.EXPUNGED/*' savedsince 1d

autoexpunge

Set autoexpunge configuration for expunge storage to automatically clean old messages.

See Namespaces.

Obox Settings

Lazy expunge allows reduction of Cassandra dictmap lookups by removing the lockdir setting and enabling the obox_track_copy_flags setting.

 mail_plugins = $mail_plugins lazy_expunge
 plugin {
   lazy_expunge = .EXPUNGED
   # If Cassandra w/obox is used:
   obox_track_copy_flags = yes
}

See obox_track_copy_flags.

Dumpster

See Dumpster Config for information on how to configure lazy_expunge with the OX Dumpster module.