# Ticket Field and Email Hooks

https://dev.wisecp.com/es/ticket-field-and-mail-hooks

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|null**A non-empty text blocks the operation** and is shown as the error. An empty return lets it carry on.

Listener PHP

```php
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 PHP

```php
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|null**A non-empty text blocks the operation** and is shown as the error. An empty return lets it carry on.

Listener PHP

```php
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 PHP

```php
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 PHP

```php
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 PHP

```php
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|null**A 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 PHP

```php
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|null**A non-empty text stops the ticket being opened**. Same shape as the mail gate: nothing is thrown, the job is recorded as skipped.

Listener PHP

```php
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 PHP

```php
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.

## Related Articles

- Support Ticket Hooks
- [Customer Account Hooks](https://dev.wisecp.com/en/customer-account-hooks)
- [How Hooks Work](https://dev.wisecp.com/en/how-hooks-work)
