Ticket Field and Email Hooks

1 views Markdown

The nine hooks over custom fields and tickets arriving by email.

Overview

Two subjects meet here: the custom fields an operator defines, and the handling of tickets arriving by email.

The email gates stand apart from other gates: your block raises no error and is skipped silently. It stops the unwanted message but leaves no trace, so you must record the reason yourself.

Reference

Stopping a custom field being saved

gateticket.custom_field.save
AdminTickets zero means new

Runs before a ticket custom field is saved.

Parameters 3
$idintThe id being edited; a zero on a new record.
$didintThe department it belongs to; a zero makes it show in every department.
$typestringThe field type: text, long text, password, select, radio or checkbox.
Return 1
string|nullA non-empty text blocks the operation and is shown as the error. An empty return lets it carry on.
Listener
Hook::add('gate:ticket.custom_field.save', 10, function ($id, $did, $type) {
    // A zero department means it shows everywhere.
    if ($type === 'password' && $did === 0)
        return 'A password field cannot be opened to every department.';

    return null;
});

Following a custom field being saved

actionticket.custom_field.saved
AdminTickets language data separate

Runs after a custom field is saved.

Parameters 2
$fieldarrayThe structural record: id, department, status and rank. Its name and description are not here: they live in the language record and are read separately.
$isNewboolTrue when newly created.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:ticket.custom_field.saved', 10, function ($field, $isNew) {
    // Name and description live in the language record, not here.
    Acme::syncFieldSchema((int) ($field['id'] ?? 0));
});

Stopping a custom field deletion

gateticket.custom_field.delete
AdminTickets always a list

Runs before custom fields are deleted.

Parameters 1
$idarrayThe ids to be deleted. A single deletion is still a one-element array: a check expecting a number catches nothing.
Return 1
string|nullA non-empty text blocks the operation and is shown as the error. An empty return lets it carry on.
Listener
Hook::add('gate:ticket.custom_field.delete', 10, function ($id) {
    // It is an array even for a single deletion.
    foreach ($id as $one)
        if (Acme::fieldHasData((int) $one)) return 'A field holding data cannot be deleted.';

    return null;
});

Following a custom field being deleted

actionticket.custom_field.deleted
AdminTickets language record in hand

Runs after a custom field is deleted.

Parameters 2
$iintThe id of the deleted field.
$flangarrayIts language record as taken before the deletion: name, description and options. If you need its name, this is your only source.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:ticket.custom_field.deleted', 10, function ($i, $flang) {
    // If you need the name, the language record is the only source.
    Acme::dropFieldSchema($i, $flang['name'] ?? '');
});

Changing the custom field list

filterticket.custom_fields
AdminTickets passed by link

Runs after the custom fields are read. Add a field of your own to the list here.

Parameters 2
$fieldsarrayby linkThe field rows: id, department, name, type and options.
$ctxarrayby linkContext: language, department and status filter. A zero department means every department is being asked for.
Return 1
voidThe return is ignored; you write over the data.
Listener
Hook::add('filter:ticket.custom_fields', 10, function (&$fields, &$ctx) {
    // A zero department means every department is being asked for.
    $fields = array_values(array_filter($fields,
        fn ($f) => Acme::fieldVisible((int) ($f['id'] ?? 0))));
});

Changing the group fields

filterticket.access_group_fields
AdminTickets the language is never empty

Runs after the custom fields tied to an access group are read.

Parameters 2
$fieldsarrayby linkThe group-scoped field rows.
$ctxarrayby linkContext: the language and the group id. Even when the call leaves it empty the language is filled in before the hook: it never arrives empty.
Return 1
voidThe return is ignored; you write over the data.
Listener
Hook::add('filter:ticket.access_group_fields', 10, function (&$fields, &$ctx) {
    // The language never arrives empty.
    $fields = Acme::orderFields($fields, $ctx['lang']);
});

Skipping an incoming email

gateticket.pipe_import
cronjobs a skip, not a failure

Runs before an incoming email becomes a ticket. This is where you keep unwanted mail out of the system entirely.

Parameters 3
$mailarrayThe raw email: subject, body, sender, recipient, attachments and address.
$didintThe department from the source; a zero means it will be resolved from the address.
$msgIdstringThe unique id of the message.
Return 1
string|nullA non-empty text skips the email entirely. ? Unlike other gates nothing is thrown: the scheduled task records it as skipped and the mail job is not counted as failed. Your block is silent. Record the reason yourself.
Listener
Hook::add('gate:ticket.pipe_import', 10, function ($mail, $did, $msgId) {
    // The veto is SILENT: record the reason yourself.
    if (Acme::isAutoReply($mail)) {
        Acme::log('auto-reply skipped', $msgId);
        return 'auto-reply';
    }

    return null;
});

Stopping a ticket opening from email

gateticket.open
cronjobs a skip, not a failure

Runs immediately before an incoming email becomes a ticket. The message has passed the mail gate and its department is resolved.

Parameters 6
$subjectstringThe ticket title, normalised.
$messagestringThe first message body.
$departmentIdintThe resolved department.
$clientIdintThe customer id; zero for an unrecognised sender.
$clientEmailstringThe sender address.
$mailarrayThe raw email object.
Return 1
string|nullA non-empty text stops the ticket being opened. Same shape as the mail gate: nothing is thrown, the job is recorded as skipped.
Listener
Hook::add('gate:ticket.open', 10,
    function ($subject, $message, $departmentId, $clientId, $clientEmail, $mail) {
        // For an unrecognised sender the customer id is ZERO.
        if (!$clientId && Acme::strangersBlocked()) return 'unrecognised sender';

        return null;
    });

Cleaning the text from an email

filterticket.pipe_message_text
cronjobs both by link

Runs before the body and subject of an incoming email are written to a ticket. Trimming quoted blocks and signatures belongs here.

Parameters 3
$messagestringby linkThe normalised body.
$subjectstringby linkThe ticket subject; the reference tag is already stripped.
$mailarrayThe raw email object, there for context.
Return 1
voidThe return is ignored; both are passed by link and both may be changed.
Listener
Hook::add('filter:ticket.pipe_message_text', 10, function (&$message, &$subject, $mail) {
    // Both are passed by link.
    $message = Acme::stripQuotedReply($message);
});

Pitfalls

The veto in the email gates is silent

When other gates block, an error is thrown and the user sees the reason. The email gates do not work that way: your block is recorded as skipped, the job is not counted as failed and nobody sees anything. It is the right place to stop unwanted mail, but without recording the reason yourself no trace remains.

The field name lives in the language record

The custom field save event gives the structural row: id, department, status, rank. The name and description are not there; they sit in the language record. A listener looking for the name works with an empty value.

Was this helpful?

Thanks for your feedback!

Still Need Help?

Our support team is here around the clock for anything you can't find above.