mirror of
https://github.com/itflow-org/itflow
synced 2026-11-15 08:57:23 +00:00
Emails stage 2: agent and staff emails in the shared email layout with an Open in ITFlow button - assigned to you (assignee pick, Bulk Action, New Ticket and Edit, several at once as one list, recurring tickets the same way, nothing when assigning yourself), new ticket to the new-ticket address (portal, email or scheduled, with what they wrote), client replied to the assigned agent (portal or email, with the reply), SLA at risk and missed to the SLA alert address and the assigned agent, scheduled work and its cancellation to the agent, contact and watchers with the calendar file, a new agent's sign in details, account changes, new device and failed MFA sign in alerts, a login sent back through a shared link, the Admin > Mail test email and the portal's saved card confirmation; nothing is queued when outgoing mail is not set up; fixes backslashes before apostrophes in the parser's new ticket and reply emails, emails skipped when no SMTP host was set for Microsoft 365 or Google mail, and the saved card email's wording; adds emailSettings(), emailReady() and cal_str to functions/email.php, the ticket agent email functions, userMailNewAccount() and userMailSignInAlert(), a new sendSlaAlert() signature, removes userQueueMail() and updates the changelog
This commit is contained in:
22
CHANGELOG.md
22
CHANGELOG.md
@@ -161,6 +161,8 @@ If you are coming from 26.08 or earlier, the database update still has to be run
|
||||
Click the value and pick a new one, or use the + at the end of a record's tags. Each change saves straight away.
|
||||
- Tickets: a closed ticket can no longer be changed, either from the ticket's windows or through the API. Merging is only offered between tickets of the same client.
|
||||
- Tickets: a ticket that closes by itself some time after it was resolved now sends the client the closed email, as closing it yourself does, when client notifications are on (Admin > Ticket). It used to close without a word.
|
||||
- Tickets: an agent is now emailed when a ticket is assigned to them from New Ticket or Edit, as they already were from the assignee pick and Bulk Action. It used to be an app notice only. Assigning a ticket to yourself sends nothing.
|
||||
- Emails to agents keep their subjects, so mail rules still match. The ones that changed: a client's reply from the portal is "ITFlow - Ticket updated - [TCK-12] Subject", with the dash the emailed reply always had; one recurring ticket assigned from Bulk Action is "ITFlow - Recurring ticket Subject assigned to you", as from the assignee pick; and a ticket raised on a schedule reaches the new-ticket address as "ITFlow - New Recurring Ticket - Client: Subject".
|
||||
- Client portal: primary and technical contacts' Tickets page now opens on everyone's tickets at their company, with Mine to see only their own. The separate All tickets page is gone, and links to it land on Tickets. A ticket shows the client where it stands - Received, In progress, Waiting on you, On hold, Resolved or Closed - rather than your ticket statuses.
|
||||
- Client portal: invoices and quotes now open in the portal, where they used to open their guest link in a new tab. The links in your emails work as before. A quote answered in the portal records the contact's name and any note they left. Pay invoices pays the invoices ticked as one card payment, and the Pay drop-down on each invoice is gone (see Client portal).
|
||||
- Client portal: only the primary contact can add or change contacts now, and give or turn off their portal access. Technical contacts still see the contacts, but can no longer add or edit them. A primary contact can turn access back on for someone you turned off, and it is logged and notified when they do.
|
||||
@@ -846,6 +848,16 @@ If you are coming from 26.08 or earlier, the database update still has to be run
|
||||
- Resolved says when it closes by itself, with a button to view or reopen it. Closed asks how you did.
|
||||
- The subjects are the same and the reply line stays at the top, so replies still go onto their ticket and are cut off where they were.
|
||||
- Images in a reply point at your ITFlow, so they show, and anything in a reply that could run in the email is taken out.
|
||||
- Scheduled work: the contact, the ticket's watchers and the assigned agent get the date, time and whether it is on site, with the calendar file as before. Cancelling it tells them the same way, and the calendar file takes it out of their calendar.
|
||||
- Emails to your team are in the same look, each with an Open in ITFlow button:
|
||||
- Assigned to you: who assigned it, with the details and the client. Several from Bulk Action come as one email listing them, with a button to the agent's tickets. Recurring tickets the same way.
|
||||
- New ticket, to the new-ticket address (Admin > Ticket): who raised it and how - in the portal, by email or on a schedule - with what they wrote. A ticket raised on a schedule now reaches it too.
|
||||
- Client replied, to the agent the ticket is assigned to: who replied, in the portal or by email, with what they wrote. A portal reply used to say only to check ITFlow.
|
||||
- SLA at risk and missed, to the SLA alert address (Admin > SLAs) and the assigned agent: which target, when it is or was due and who has it, in amber for at risk and red once missed. An agent who is also the alert address gets one.
|
||||
- Shared links: a login sent back through a link you made, with a button to review it.
|
||||
- Account emails: a new agent's sign in details with a Sign in button (and that the email has their password in it), a change to their account, a sign in from a new device and a wrong MFA code. The sign in alerts say when, the browser and system in words, and the IP address, with a button to their sign in security.
|
||||
- The test email in Admin > Mail comes in the new look, saying which mailbox it was sent from.
|
||||
- Client portal: the email confirming a saved card is in the billing emails' look, from the invoice mailbox, naming the card and when it expires.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -951,6 +963,9 @@ If you are coming from 26.08 or earlier, the database update still has to be run
|
||||
- Roles: the built-in Accountant role couldn't take a payment, email a reminder or statement, or delete an expense or other income entered by mistake.
|
||||
- Tickets: a ticket raised from a recurring ticket never emailed its contact or the new-ticket address, as the settings it read were never loaded.
|
||||
- Tickets: the email telling someone their email had become a ticket showed an apostrophe in its subject, or in their name, with a backslash in front of it.
|
||||
- Tickets: SLA alert emails were queued when outgoing mail was not set up. For a ticket raised or answered by email, the new ticket and client replied emails showed an apostrophe in the subject, the client's name or the reply with a backslash in front of it.
|
||||
- Admin > Mail: the test email was queued when outgoing mail was not set up, or when what was typed was not an email address, and greeted everyone as "Chap".
|
||||
- Emails: a saved card's confirmation, a new agent's sign in details and the email about a change to an agent's account were only sent when an SMTP host was filled in, so they were skipped when mail goes through Microsoft 365 or Google with no host. The saved card's email began "Were writing".
|
||||
- Contacts: an agent without Support saw a contact's Assets, Documents, Files and Services tabs, which led to pages they were refused. A document's or file's linked assets, licenses, services, domains and certificates were listed to them too.
|
||||
- All Client Docs listed Credentials to agents who can't open it.
|
||||
- Notifications: shared-link notifications reached agents without Support, with a link they couldn't open.
|
||||
@@ -1379,7 +1394,12 @@ If you are coming from 26.08 or earlier, the database update still has to be run
|
||||
- Emails:
|
||||
- New `functions/email.php`, loaded by `functions.php`: `emailHtml($email)` draws every email - `billingEmailHtml()` is now a wrapper for it that greets nobody without a name - with new fields `marker` (the reply line, first), `preview` (inbox preview text), `message` (`html`, `by`, `when`, `label` - HTML someone wrote, through `emailMessageHtml()`), `callout`, `contact` (the foot's contact line) and the `secondary` tone, and `name` '' for "Hello,". `EMAIL_TONES` and `EMAIL_REPLY_MARKER`. `emailMessageHtml($html)` purifies it (`documentHtmlPurify()`), drops the reply line, makes `/` links and images absolute, and caps image widths. `emailFrom($which)` (`system` or `ticket`, read from the settings as saved). `emailQueue($recipients, $subject, $email, $opts)` - recipients with `email`, `name`, `greet` and `footnote`, raw values, escaped for `addToMailQueue()`, each address once; `from` and `attachments` in `$opts`.
|
||||
- `functions/ticket.php`: `ticketEmailContent($row, $kind, $opts)` (`created` with `how` agent, email or scheduled, `reply`, `resolved`, `closed`, `watcher`) and `ticketMailSend($ticket_id, $subject, $email, $opts)` (`attachments`, `to_contact`, `to_watchers`, `to`, `kind`) replace `ticketMailContactAndWatchers()` and `ticketMailCompany()`. Also `ticketEmailFrom()`, `ticketEmailUrl()`, `ticketEmailContactLine()`, `ticketEmailSla()` (in place of `getTicketSlaEmailNotice()`, removed) and `ticketMailClosed()`. `ticketMailResolved($ticket_id, $note)` takes the note as raw HTML. The reply handlers read the reply back from `ticket_replies` for the email, so it carries the signature as saved.
|
||||
- `recurringTicketRaise()` declares the ticket settings it reads as globals. `cron/nightly_tasks.php` calls `ticketMailClosed()` for each ticket it closes. `cron/ticket_email_parser.php`'s new-ticket reply and closed-ticket bounce, `api/v1/ticket_replies/create.php`, `add_ticket_watcher`, `bulk_ticket_reply`, `bulk_resolve_tickets` and `add_ticket_task_approver` go through them too.
|
||||
- `recurringTicketRaise()` declares `$config_ticket_client_general_notifications` as a global, and tells the new-ticket address through `ticketMailNewTicketAddress()`. `cron/nightly_tasks.php` calls `ticketMailClosed()` for each ticket it closes. `cron/ticket_email_parser.php`'s new-ticket reply and closed-ticket bounce, `api/v1/ticket_replies/create.php`, `add_ticket_watcher`, `bulk_ticket_reply`, `bulk_resolve_tickets` and `add_ticket_task_approver` go through them too.
|
||||
- `functions/email.php`: `emailSettings()` reads the mail settings from the database once per request, and `emailReady()` says whether outgoing mail is set up - so cron scripts, which keep escaped copies of the settings or none, can use them. `emailFrom()` reads through it. `emailQueue()` takes `cal_str` in `$opts`.
|
||||
- `functions/ticket.php`, emails to agents: `ticketAgentEmailContent($row, $kind, $opts)` (`new` with `how` portal, email or scheduled; `replied` with `how`, `message` and `by`; `assigned` with `by`; `scheduled` with `when`, `where` and `cancelled`; `sla` with `track`, `breached` and `due`), `ticketAgentUrl($row)`, `ticketMailAgent($user_id)` (an active agent's email and name, or null), `ticketMailAgents($recipients, $subject, $email)` (from the ticket mailbox), `ticketMailNewTicketAddress($ticket_id, $how)`, `ticketMailAssigneeReplied($ticket_id, $how, $reply_html, $by)`, `ticketMailAssigned($user_id, $ticket_ids, $by)` and `recurringTicketMailAssigned($user_id, $subjects, $by)` - not when an agent assigns themselves. `ticketEmailContent()` has a `scheduled` kind, and `ticketMailSend()` passes `cal_str`. `ticketMailSchedule()` sends the agent's email through `ticketAgentEmailContent()` and the client's through `ticketMailSend()`, both with the calendar file.
|
||||
- Callers: `ticketChangeAssignee()`, `recurringTicketChangeAssignee()`, `bulk_assign_ticket`, `add_ticket` and `edit_ticket` (`agent/post/ticket.php`), `recurringTicketNotifyAgent()` (`agent/post/recurring_ticket.php`), the portal's `add_ticket`, `add_ticket_comment` and `stripe_save_card` (`client/post.php`, the last through `billingEmailQueue()`), the parser's `addTicket()` and `addReply()`, `login.php`'s sign in alerts, `add_user`, `shareNotifyCreator()` and `test_email_smtp`.
|
||||
- `cron/ticket_sla.php`: `sendSlaAlert($ticket, $track, $breached, $due)` - `$track` is `response` or `resolution` - in place of `sendSlaAlert($ticket, $subject_line, $body_line)`. `$from_email` and `$from_name` are gone.
|
||||
- `functions/user.php`: `userSignInUrl()` (with the login key when one is required), `userMailAppName()`, `userMailNewAccount($name, $email, $password, $force_mfa)` (true when queued) and `userMailSignInAlert($name, $email, $kind, $ip, $user_agent)` (`new` or `mfa`). `userNotifyChange()` sends through `emailQueue()`. `userQueueMail()` is removed.
|
||||
- Empty states: every list page sets `$empty_state` before including `includes/filter_footer.php`, and `includes/inc_empty_state.php` draws it with `emptyStateHtml()` (`functions/format.php`) when the list has nothing - `thing`, `icon`, `title`, `text`, `actions` (`label`, `modal` or `href`, `size`, `icon`; the first is the main one), `archived`, `view`, `filtered`, `views`, `match_title`, `match_text` and `clear_url`. `emptyStateFiltered($views)` reads the GET string for a search or filter, leaving out paging, sorting, `client_id`, `archived` and the page's views, and `emptyStateClearUrl($views)` is the page without them. `emptyStatePill($pills, $key, $default, $good)` builds a pill's own state from the page's pill array (`label`, `icon`, `title`, `alarm`), with a tick for `alarm` pills and the keys in `$good`. Files, notifications and racks call `emptyStateHtml()` in their own layout; files and notifications set `$empty_state_skip`. A page that sets nothing still gets one, worded from `$page_title`. The pages' own empty blocks (`.itflow-asset-empty` under a list) are gone. CSS: `.itflow-empty-state` with `-icon` (a grey round tile), `-title`, `-text` and `-actions`.
|
||||
- Removed unused helpers and files:
|
||||
- `credentialLink()`, `projectLink()`, `linkCounts()`, `assetContactAvatar()`, `clientOverviewPersonInitials()`, and `getMonthlyTax()` and `getQuarterlyTax()`, which looked a tax up by name
|
||||
|
||||
@@ -217,41 +217,27 @@ if (isset($_POST['test_email_smtp'])) {
|
||||
|
||||
$test_email = intval($_POST['test_email']);
|
||||
|
||||
if($test_email == 1) {
|
||||
$email_from = escapeSql($config_mail_from_email);
|
||||
$email_from_name = escapeSql($config_mail_from_name);
|
||||
} elseif ($test_email == 2) {
|
||||
$email_from = escapeSql($config_invoice_from_email);
|
||||
$email_from_name = escapeSql($config_invoice_from_name);
|
||||
} elseif ($test_email == 3) {
|
||||
$email_from = escapeSql($config_quote_from_email);
|
||||
$email_from_name = escapeSql($config_quote_from_name);
|
||||
} else {
|
||||
$email_from = escapeSql($config_ticket_from_email);
|
||||
$email_from_name = escapeSql($config_ticket_from_name);
|
||||
// From the mailbox picked - the system's, invoices', quotes' or tickets'
|
||||
$which = [1 => 'the system mailbox', 2 => 'the invoice mailbox', 3 => 'the quote mailbox'][$test_email] ?? 'the ticket mailbox';
|
||||
$from = $test_email == 1 ? emailFrom() : ($test_email == 2 ? billingEmailFrom('invoice') : ($test_email == 3 ? billingEmailFrom('quote') : emailFrom('ticket')));
|
||||
$to = trim((string) ($_POST['email_to'] ?? ''));
|
||||
|
||||
if (!emailReady()) {
|
||||
flashAlert('Set up how mail is sent first, then send the test', 'error');
|
||||
redirect($mail_tab_redirect);
|
||||
}
|
||||
|
||||
$email_to = escapeSql($_POST['email_to']);
|
||||
$subject = "Test email from ITFlow";
|
||||
$body = "This is a test email from ITFlow. If you are reading this, it worked!";
|
||||
$sent = emailQueue([['email' => $to, 'greet' => '', 'footnote' => 'You get this because someone sent a test from Admin > Mail in ITFlow.']], "Test email from ITFlow", [
|
||||
'kind' => 'Test', 'tone' => 'success', 'preview' => "A test from $which",
|
||||
'title' => 'Mail is working',
|
||||
'intro' => ["This is a test email from ITFlow, sent from $which. If you are reading this, it worked."],
|
||||
'summary' => ['From' => trim($from['name'] . ' <' . $from['email'] . '>'), 'To' => $to, 'Sent' => date('D j M Y, g:i A')],
|
||||
], ['from' => $from]);
|
||||
|
||||
$data = [
|
||||
[
|
||||
'from' => $email_from,
|
||||
'from_name' => $email_from_name,
|
||||
'recipient' => $email_to,
|
||||
'recipient_name' => 'Chap',
|
||||
'subject' => $subject,
|
||||
'body' => $body
|
||||
]
|
||||
];
|
||||
|
||||
$mail = addToMailQueue($data);
|
||||
|
||||
if ($mail === true) {
|
||||
if ($sent) {
|
||||
flashAlert("Test email queued! Check <strong>Admin > Mail queue</strong>");
|
||||
} else {
|
||||
flashAlert("Failed to add test mail to queue", 'error');
|
||||
flashAlert(escapeHtml($to) . " isn't an email address - nothing was queued", 'error');
|
||||
}
|
||||
|
||||
redirect($mail_tab_redirect);
|
||||
|
||||
@@ -62,19 +62,12 @@ if (isset($_POST['add_user'])) {
|
||||
}
|
||||
|
||||
// Their sign in details by email - the password goes as typed, so it is theirs to change
|
||||
if (!empty($_POST['send_email']) && !empty($config_smtp_host)) {
|
||||
$company_name = (string) mysqli_fetch_row(mysqli_query($mysqli, "SELECT company_name FROM companies WHERE company_id = 1"))[0];
|
||||
$login_url = "https://$config_base_url/login.php" . ($config_login_key_required == 1 ? "?key=$config_login_key_secret" : '');
|
||||
$mail = userQueueMail($email, $name, "Your new $company_name ITFlow account",
|
||||
'Hello ' . escapeHtml($name) . ',<br><br>An ITFlow account has been set up for you. Please change your password after you sign in'
|
||||
. ($force_mfa ? ', and have an authenticator app ready - you will set up multi-factor authentication the first time' : '') . '.<br><br>'
|
||||
. 'Email: ' . escapeHtml($email) . '<br>Password: ' . escapeHtml($password) . '<br>Sign in at: ' . escapeHtml($login_url)
|
||||
. '<br><br>--<br>' . escapeHtml($company_name) . ' - Support<br>' . escapeHtml($config_ticket_from_email));
|
||||
if ($mail === true) {
|
||||
if (!empty($_POST['send_email']) && emailReady()) {
|
||||
if (userMailNewAccount($name, $email, $password, $force_mfa)) {
|
||||
$notes[] = 'their sign in details were emailed to them';
|
||||
} else {
|
||||
$notes[] = 'the email with their sign in details could not be queued';
|
||||
logApp('Mail', 'error', 'Failed to queue the new account email for ' . $email . ": $mail");
|
||||
logApp('Mail', 'error', 'Failed to queue the new account email for ' . escapeSql($email));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ function recurringTicketSetTasks($recurring_ticket_id) {
|
||||
|
||||
/** Tell an agent some recurring tickets are theirs now - not themselves, not nobody. $subjects are raw. */
|
||||
function recurringTicketNotifyAgent($user_id, array $subjects) {
|
||||
global $mysqli, $session_user_id, $session_name, $session_company_name, $config_smtp_provider, $config_ticket_from_email, $config_ticket_from_name, $config_app_name;
|
||||
global $mysqli, $session_user_id, $session_name;
|
||||
$user_id = intval($user_id);
|
||||
if (!$user_id || $user_id == $session_user_id || !$subjects) {
|
||||
return;
|
||||
@@ -85,18 +85,7 @@ function recurringTicketNotifyAgent($user_id, array $subjects) {
|
||||
$what = $count === 1 ? 'Recurring ticket ' . escapeSql($subjects[0]) . ' has' : "$count recurring tickets have";
|
||||
mysqli_query($mysqli, "INSERT INTO notifications SET notification_type = 'Recurring Ticket', notification = '$what been assigned to you by " . escapeSql($session_name) . "', notification_action = 'recurring_tickets.php?assigned_agent=$user_id', notification_user_id = $user_id");
|
||||
|
||||
$agent = quickPickAgent($user_id);
|
||||
if ($agent && !empty($config_smtp_provider) && filter_var($agent['user_email'], FILTER_VALIDATE_EMAIL)) {
|
||||
$list = implode('<br>', array_map(function ($subject) { return escapeSql(escapeHtml($subject)); }, $subjects));
|
||||
addToMailQueue([[
|
||||
'from' => escapeSql($config_ticket_from_email),
|
||||
'from_name' => escapeSql($config_ticket_from_name),
|
||||
'recipient' => escapeSql($agent['user_email']),
|
||||
'recipient_name' => escapeSql($agent['user_name']),
|
||||
'subject' => escapeSql("$config_app_name - $count recurring ticket" . ($count === 1 ? '' : 's') . " assigned to you"),
|
||||
'body' => "Hi " . escapeSql($agent['user_name']) . ", <br><br>" . escapeSql($session_name) . " assigned $count recurring ticket" . ($count === 1 ? '' : 's') . " to you:<br><br>$list<br><br>Thanks, <br>" . escapeSql($session_name) . "<br>" . escapeSql($session_company_name),
|
||||
]]);
|
||||
}
|
||||
recurringTicketMailAssigned($user_id, $subjects, stripslashes($session_name));
|
||||
}
|
||||
|
||||
if (isset($_POST['add_recurring_ticket'])) {
|
||||
|
||||
@@ -180,6 +180,7 @@ if (isset($_POST['add_ticket'])) {
|
||||
if ($assigned_to) {
|
||||
$row = ticketRow($ticket_id);
|
||||
ticketNotifyAgent($assigned_to, $row, "Ticket " . ticketReference($row) . " - Subject: " . $row['ticket_subject'] . " has been assigned to you by $session_name");
|
||||
ticketMailAssigned($assigned_to, [$ticket_id], stripslashes($session_name));
|
||||
}
|
||||
|
||||
logAudit("Ticket", "Create", "$session_name created ticket $ticket_prefix$ticket_number - $subject", $client_id, $ticket_id);
|
||||
@@ -290,6 +291,7 @@ if (isset($_POST['edit_ticket'])) {
|
||||
if ($assigned_to) {
|
||||
logTicketHistory($ticket_id, "$session_name assigned the ticket to " . escapeSql(getFieldById('users', $assigned_to, 'user_name')));
|
||||
ticketNotifyAgent($assigned_to, $row, "Ticket " . ticketReference($row) . " - Subject: " . $subject_input . " has been assigned to you by $session_name");
|
||||
ticketMailAssigned($assigned_to, [$ticket_id], stripslashes($session_name));
|
||||
} else {
|
||||
logTicketHistory($ticket_id, "$session_name unassigned the ticket");
|
||||
}
|
||||
@@ -620,7 +622,7 @@ if (isset($_POST['bulk_assign_ticket'])) {
|
||||
$agent_name_sql = escapeSql($agent_name);
|
||||
|
||||
$count = $skipped = 0;
|
||||
$assigned_list = '';
|
||||
$assigned_ids = [];
|
||||
foreach (ticketsForBulk(2) as $ticket_id => $row) {
|
||||
// A closed ticket keeps who closed it
|
||||
if (!empty($row['ticket_closed_at'])) {
|
||||
@@ -644,7 +646,7 @@ if (isset($_POST['bulk_assign_ticket'])) {
|
||||
|
||||
triggerCustomAction('ticket_assign', $ticket_id);
|
||||
|
||||
$assigned_list .= escapeSql(ticketReference($row) . ' - ' . $row['ticket_subject']) . "<br>";
|
||||
$assigned_ids[] = $ticket_id;
|
||||
$count++;
|
||||
}
|
||||
|
||||
@@ -652,18 +654,7 @@ if (isset($_POST['bulk_assign_ticket'])) {
|
||||
if ($count && $assign_to && $assign_to != $session_user_id) {
|
||||
mysqli_query($mysqli, "INSERT INTO notifications SET notification_type = 'Ticket', notification = '$count ticket" . ($count === 1 ? ' has' : 's have') . " been assigned to you by $session_name', notification_action = '/agent/tickets.php?assigned=$assign_to', notification_user_id = $assign_to");
|
||||
|
||||
$agent_email = (string) getFieldById('users', $assign_to, 'user_email');
|
||||
if (!empty($config_smtp_provider) && filter_var($agent_email, FILTER_VALIDATE_EMAIL)) {
|
||||
$company_name = escapeSql($session_company_name);
|
||||
addToMailQueue([[
|
||||
'from' => escapeSql($config_ticket_from_email),
|
||||
'from_name' => escapeSql($config_ticket_from_name),
|
||||
'recipient' => escapeSql($agent_email),
|
||||
'recipient_name' => $agent_name_sql,
|
||||
'subject' => escapeSql($config_app_name) . " - $count tickets have been assigned to you",
|
||||
'body' => "Hi $agent_name_sql, <br><br>$session_name assigned $count tickets to you!<br><br>$assigned_list<br>Thanks, <br>$session_name<br>$company_name",
|
||||
]]);
|
||||
}
|
||||
ticketMailAssigned($assign_to, $assigned_ids, stripslashes($session_name));
|
||||
}
|
||||
|
||||
flashAlert("Assigned <strong>$count</strong> ticket" . ($count === 1 ? '' : 's') . " to <strong>" . escapeHtml($agent_name) . "</strong>" . ($skipped ? ", <strong>$skipped</strong> closed left as they were" : ''));
|
||||
|
||||
100
client/post.php
100
client/post.php
@@ -94,27 +94,8 @@ if (isset($_POST['add_ticket'])) {
|
||||
// Screenshots and files sent with it belong to the ticket itself
|
||||
saveTicketAttachments($ticket_id, null, 'file');
|
||||
|
||||
// Notify agent DL of the new ticket, if populated with a valid email
|
||||
if ($config_ticket_new_ticket_notification_email) {
|
||||
|
||||
$client_name = escapeSql($session_client_name);
|
||||
|
||||
$email_subject = "ITFlow - New Ticket - $client_name: $subject";
|
||||
$email_body = "Hello, <br><br>This is a notification that a new ticket has been raised in ITFlow. <br>Client: $client_name<br>Priority: $priority<br>Link: https://$config_base_url/agent/ticket.php?ticket_id=$ticket_id&client_id=$session_client_id <br><br><b>$subject</b><br>$details";
|
||||
|
||||
// Queue Mail
|
||||
$data = [
|
||||
[
|
||||
'from' => $config_ticket_from_email,
|
||||
'from_name' => $config_ticket_from_name,
|
||||
'recipient' => $config_ticket_new_ticket_notification_email,
|
||||
'recipient_name' => $config_ticket_from_name,
|
||||
'subject' => $email_subject,
|
||||
'body' => $email_body,
|
||||
]
|
||||
];
|
||||
addToMailQueue($data);
|
||||
}
|
||||
// The new-ticket address (Admin > Ticket), when there is one
|
||||
ticketMailNewTicketAddress($ticket_id, 'portal');
|
||||
|
||||
// The primary and technical contacts who asked to hear about new tickets at their company
|
||||
ticketMailColleaguesNew($ticket_id);
|
||||
@@ -164,38 +145,8 @@ if (isset($_POST['add_ticket_comment'])) {
|
||||
}
|
||||
|
||||
|
||||
// Get ticket details & Notify the assigned tech (if any)
|
||||
$ticket_details = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT client_name, ticket_assigned_to, ticket_number, ticket_subject FROM tickets LEFT JOIN clients ON ticket_client_id = client_id WHERE ticket_id = $ticket_id LIMIT 1"));
|
||||
|
||||
$ticket_number = intval($ticket_details['ticket_number']);
|
||||
$ticket_assigned_to = intval($ticket_details['ticket_assigned_to']);
|
||||
$ticket_subject = escapeSql($ticket_details['ticket_subject']);
|
||||
$client_name = escapeSql($ticket_details['client_name']);
|
||||
|
||||
if ($ticket_details && $ticket_assigned_to !== 0) {
|
||||
|
||||
// Get tech details
|
||||
$tech_details = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT user_email, user_name FROM users WHERE user_id = $ticket_assigned_to LIMIT 1"));
|
||||
$tech_email = escapeSql($tech_details['user_email']);
|
||||
$tech_name = escapeSql($tech_details['user_name']);
|
||||
|
||||
$subject = "$config_app_name Ticket updated - [$config_ticket_prefix$ticket_number] $ticket_subject";
|
||||
$body = "Hello $tech_name,<br><br>A new reply has been added to the below ticket, check ITFlow for full details.<br><br>Client: $client_name<br>Ticket: $config_ticket_prefix$ticket_number<br>Subject: $ticket_subject<br><br>https://$config_base_url/agent/ticket.php?ticket_id=$ticket_id&client_id=$session_client_id";
|
||||
|
||||
$data = [
|
||||
[
|
||||
'from' => $config_ticket_from_email,
|
||||
'from_name' => $config_ticket_from_name,
|
||||
'recipient' => $tech_email,
|
||||
'recipient_name' => $tech_name,
|
||||
'subject' => $subject,
|
||||
'body' => $body
|
||||
]
|
||||
];
|
||||
|
||||
addToMailQueue($data);
|
||||
|
||||
}
|
||||
// The agent it is assigned to, with what they wrote
|
||||
ticketMailAssigneeReplied($ticket_id, 'portal', (string) getFieldById('ticket_replies', $ticket_reply_id, 'ticket_reply'), stripslashes($session_contact_name));
|
||||
|
||||
// Store any attached files against this reply
|
||||
saveTicketAttachments($ticket_id, $ticket_reply_id, 'file');
|
||||
@@ -1269,39 +1220,16 @@ if (isset($_GET['stripe_save_card'])) {
|
||||
redirect("saved_payment_methods.php");
|
||||
}
|
||||
|
||||
// Email Confirmation
|
||||
$sql_settings = mysqli_query($mysqli, "
|
||||
SELECT company_name, company_phone, company_phone_country_code, config_invoice_from_email,
|
||||
config_invoice_from_name, config_smtp_host FROM companies, settings
|
||||
WHERE companies.company_id = settings.company_id
|
||||
AND companies.company_id = 1
|
||||
");
|
||||
$row = mysqli_fetch_assoc($sql_settings);
|
||||
|
||||
$company_name = escapeSql($row['company_name']);
|
||||
$company_phone = escapeSql(formatPhoneNumber($row['company_phone'], $row['company_phone_country_code']));
|
||||
$config_invoice_from_email = escapeSql($row['config_invoice_from_email']);
|
||||
$config_invoice_from_name = escapeSql($row['config_invoice_from_name']);
|
||||
|
||||
if (!empty($row['config_smtp_host'])) {
|
||||
$subject = "Payment method saved";
|
||||
$body = "Hello $session_contact_name<br><br>
|
||||
Were writing to confirm that your payment details have been securely stored with Stripe our trusted payment processor.<br><br>
|
||||
You authorized us to automatically bill your card ($saved_payment_description) for future invoices.<br><br>
|
||||
You may update or remove your payment method at any time via the client portal.<br><br>
|
||||
Thank you for your business!<br><br>
|
||||
--<br>$company_name - Billing Department<br>$config_invoice_from_email<br>$company_phone";
|
||||
|
||||
$data = [[
|
||||
'from' => $config_invoice_from_email,
|
||||
'from_name' => $config_invoice_from_name,
|
||||
'recipient' => $session_contact_email,
|
||||
'recipient_name' => $session_contact_name,
|
||||
'subject' => $subject,
|
||||
'body' => $body
|
||||
]];
|
||||
|
||||
$mail = addToMailQueue($data);
|
||||
// Their confirmation, in the billing emails' look - from the invoice mailbox
|
||||
if (billingEmailReady()) {
|
||||
billingEmailQueue([['email' => stripslashes($session_contact_email), 'name' => stripslashes($session_contact_name)]], 'Payment method saved', [
|
||||
'kind' => 'Payment method', 'tone' => 'success', 'preview' => 'Your card is saved for future invoices',
|
||||
'title' => 'Your payment method is saved',
|
||||
'intro' => ['Your payment details are stored securely with Stripe, our payment processor, and you authorized us to bill this card automatically for future invoices.'],
|
||||
'summary' => ['Card' => ucfirst(stripslashes((string) $card_brand)) . ' ending ' . stripslashes((string) $last4), 'Expires' => sprintf('%02d/%d', intval($exp_month), intval($exp_year))],
|
||||
'after' => ['You can change or remove it at any time in the client portal.', 'Thank you for your business!'],
|
||||
'footnote' => billingEmailFootnote(stripslashes((string) $session_client_name)),
|
||||
]);
|
||||
}
|
||||
|
||||
logAudit("Stripe", "Update", "$session_contact_name saved payment method ($saved_payment_description) (PM: $payment_method_id)", $session_client_id);
|
||||
|
||||
@@ -71,7 +71,7 @@ $max_inline_embed_bytes = 2097152; // 2 MB - larger inline images are saved as r
|
||||
* Ticket / Reply helpers (unchanged)
|
||||
* ------------------------------------------------------------------ */
|
||||
function addTicket($contact_id, $contact_name, $contact_email, $client_id, $date, $subject, $message, $attachments, $original_message_file, $ccs) {
|
||||
global $mysqli, $config_app_name, $company_name, $company_phone, $config_ticket_prefix, $config_ticket_client_general_notifications, $config_ticket_new_ticket_notification_email, $config_base_url, $config_ticket_from_name, $config_ticket_from_email, $config_ticket_default_billable, $allowed_extensions;
|
||||
global $mysqli, $config_app_name, $company_name, $company_phone, $config_ticket_prefix, $config_ticket_client_general_notifications, $config_base_url, $config_ticket_from_name, $config_ticket_from_email, $config_ticket_default_billable, $allowed_extensions;
|
||||
$bad_pattern = "/do[\W_]*not[\W_]*reply|no[\W_]*reply/i"; // Email addresses to ignore
|
||||
|
||||
// Atomically increment and get the new ticket number
|
||||
@@ -164,32 +164,8 @@ function addTicket($contact_id, $contact_name, $contact_email, $client_id, $date
|
||||
['kind' => 'created', 'to_watchers' => false, 'to' => [['email' => stripslashes($contact_email), 'name' => stripslashes($contact_name)]]]);
|
||||
}
|
||||
|
||||
// Internal email
|
||||
$data = [];
|
||||
if ($config_ticket_new_ticket_notification_email) {
|
||||
if ($client_id == 0) {
|
||||
$client_name = "Guest";
|
||||
$client_uri = '';
|
||||
} else {
|
||||
$client_sql = mysqli_query($mysqli, "SELECT client_name FROM clients WHERE client_id = $client_id");
|
||||
$client_row = mysqli_fetch_assoc($client_sql);
|
||||
$client_name = escapeSql($client_row['client_name']);
|
||||
$client_uri = "&client_id=$client_id";
|
||||
}
|
||||
$email_subject = "$config_app_name - New Ticket - $client_name: $subject";
|
||||
$email_body = "Hello, <br><br>This is a notification that a new ticket has been raised in ITFlow. <br>Client: $client_name<br>Priority: Low (email parsed)<br>Link: https://$config_base_url/agent/ticket.php?ticket_id=$id$client_uri <br><br>--------------------------------<br><br><b>$subject</b><br>$message";
|
||||
|
||||
$data[] = [
|
||||
'from' => $config_ticket_from_email,
|
||||
'from_name' => $config_ticket_from_name,
|
||||
'recipient' => $config_ticket_new_ticket_notification_email,
|
||||
'recipient_name' => $config_ticket_from_name,
|
||||
'subject' => $email_subject,
|
||||
'body' => mysqli_real_escape_string($mysqli, $email_body)
|
||||
];
|
||||
}
|
||||
|
||||
addToMailQueue($data);
|
||||
// The new-ticket address (Admin > Ticket), when there is one
|
||||
ticketMailNewTicketAddress($id, 'email');
|
||||
|
||||
// The client's primary and technical contacts who asked to hear about new tickets
|
||||
ticketMailColleaguesNew($id);
|
||||
@@ -246,7 +222,6 @@ function addReply($from_email, $date, $subject, $ticket_number, $message, $attac
|
||||
|
||||
if ($row) {
|
||||
$ticket_id = intval($row['ticket_id']);
|
||||
$ticket_subject = escapeSql($row['ticket_subject']);
|
||||
$ticket_status = escapeSql($row['ticket_status']);
|
||||
$ticket_reply_contact = intval($row['ticket_contact_id']);
|
||||
$ticket_contact_email = escapeSql($row['contact_email']);
|
||||
@@ -256,7 +231,6 @@ function addReply($from_email, $date, $subject, $ticket_number, $message, $attac
|
||||
} else {
|
||||
$client_uri = '';
|
||||
}
|
||||
$client_name = escapeSql($row['client_name']);
|
||||
|
||||
if ($ticket_status == 5) {
|
||||
$config_ticket_prefix_esc = mysqli_real_escape_string($mysqli, $config_ticket_prefix);
|
||||
@@ -317,33 +291,9 @@ function addReply($from_email, $date, $subject, $ticket_number, $message, $attac
|
||||
}
|
||||
}
|
||||
|
||||
$ticket_assigned_to_sql = mysqli_query($mysqli, "SELECT ticket_assigned_to FROM tickets WHERE ticket_id = $ticket_id LIMIT 1");
|
||||
if ($ticket_assigned_to_sql) {
|
||||
$row3 = mysqli_fetch_assoc($ticket_assigned_to_sql);
|
||||
$ticket_assigned_to = intval($row3['ticket_assigned_to']);
|
||||
|
||||
if ($ticket_assigned_to) {
|
||||
$tech_sql = mysqli_query($mysqli, "SELECT user_email, user_name FROM users WHERE user_id = $ticket_assigned_to LIMIT 1");
|
||||
$tech_row = mysqli_fetch_assoc($tech_sql);
|
||||
$tech_email = escapeSql($tech_row['user_email']);
|
||||
$tech_name = escapeSql($tech_row['user_name']);
|
||||
|
||||
$email_subject = "$config_app_name - Ticket updated - [$config_ticket_prefix$ticket_number] $ticket_subject";
|
||||
$email_body = "Hello $tech_name,<br><br>A new reply has been added to the below ticket.<br><br>Client: $client_name<br>Ticket: $config_ticket_prefix$ticket_number<br>Subject: $ticket_subject<br>Link: https://$config_base_url/agent/ticket.php?ticket_id=$ticket_id$client_uri<br><br>--------------------------------<br>$message_esc";
|
||||
|
||||
$data = [
|
||||
[
|
||||
'from' => $config_ticket_from_email,
|
||||
'from_name' => $config_ticket_from_name,
|
||||
'recipient' => $tech_email,
|
||||
'recipient_name' => $tech_name,
|
||||
'subject' => mysqli_real_escape_string($mysqli, $email_subject),
|
||||
'body' => mysqli_real_escape_string($mysqli, $email_body)
|
||||
]
|
||||
];
|
||||
addToMailQueue($data);
|
||||
}
|
||||
}
|
||||
// The agent it is assigned to, with what they wrote
|
||||
$replied_by = intval($ticket_reply_contact) ? (string) getFieldById('contacts', intval($ticket_reply_contact), 'contact_name') : '';
|
||||
ticketMailAssigneeReplied($ticket_id, 'email', (string) getFieldById('ticket_replies', $reply_id, 'ticket_reply'), $replied_by !== '' ? $replied_by : stripslashes($from_email));
|
||||
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_status = 2, ticket_resolved_at = NULL WHERE ticket_id = $ticket_id AND ticket_client_id = $client_id LIMIT 1");
|
||||
resetTicketResolutionSla($ticket_id);
|
||||
|
||||
@@ -55,8 +55,6 @@ if ($warning_percent < 1 || $warning_percent > 99) {
|
||||
}
|
||||
|
||||
$sla_notification_email = trim(strval($sla_settings['notification_email']));
|
||||
$from_email = $sla_settings['ticket_from_email'];
|
||||
$from_name = $sla_settings['ticket_from_name'];
|
||||
|
||||
$now = time();
|
||||
|
||||
@@ -89,55 +87,35 @@ while ($running = mysqli_fetch_assoc($sql_running)) {
|
||||
syncTicketSlaClock(intval($running['ticket_id']));
|
||||
}
|
||||
|
||||
// Queue in-app + email notifications for an SLA event
|
||||
function sendSlaAlert($ticket, $subject_line, $body_line)
|
||||
// Queue in-app + email notifications for an SLA event - to the SLA alert address (Admin > SLAs) and
|
||||
// the agent the ticket is assigned to, in the email look (ticketAgentEmailContent()). $track is
|
||||
// response or resolution; $breached true once it was missed; $due when it is or was due.
|
||||
function sendSlaAlert($ticket, $track, $breached, $due)
|
||||
{
|
||||
global $mysqli, $sla_notification_email, $from_email, $from_name, $config_base_url;
|
||||
global $sla_notification_email;
|
||||
|
||||
$ticket_id = intval($ticket['ticket_id']);
|
||||
$client_id = intval($ticket['ticket_client_id']);
|
||||
|
||||
$ticket_ref = "{$ticket['ticket_prefix']}{$ticket['ticket_number']}";
|
||||
$ticket_subject = strval($ticket['ticket_subject']);
|
||||
$row = ticketRow($ticket_id);
|
||||
if (!$row) {
|
||||
return;
|
||||
}
|
||||
$ticket_ref = ticketReference($row);
|
||||
$subject_line = ucfirst($track) . ' SLA ' . ($breached ? 'breached' : 'at risk');
|
||||
|
||||
// appNotify inserts what it is given - escape at the boundary
|
||||
appNotify("Ticket SLA", escapeSql("$subject_line - $ticket_ref - $ticket_subject"), "/agent/ticket.php?ticket_id=$ticket_id", $client_id, $ticket_id);
|
||||
appNotify("Ticket SLA", escapeSql("$subject_line - $ticket_ref - " . $row['ticket_subject']), "/agent/ticket.php?ticket_id=$ticket_id", $client_id, $ticket_id);
|
||||
|
||||
// addToMailQueue also inserts raw, and the body's link markup contains
|
||||
// single quotes - escape whole strings once here. The body keeps its HTML,
|
||||
// so it gets mysqli_real_escape_string directly (escapeSql strips tags),
|
||||
// same as the ticket email parser does for its bodies.
|
||||
$email_subject = escapeSql("$subject_line: $ticket_ref - $ticket_subject");
|
||||
$email_body = mysqli_real_escape_string($mysqli, "Hello,<br><br>$body_line<br><br>Ticket: $ticket_ref<br>Subject: $ticket_subject<br><br><a href='https://$config_base_url/agent/ticket.php?ticket_id=$ticket_id'>View ticket</a>");
|
||||
|
||||
$email_data = [];
|
||||
|
||||
if (!empty($sla_notification_email)) {
|
||||
$email_data[] = [
|
||||
'from' => $from_email,
|
||||
'from_name' => escapeSql($from_name),
|
||||
'recipient' => $sla_notification_email,
|
||||
'recipient_name' => 'SLA Notifications',
|
||||
'subject' => $email_subject,
|
||||
'body' => $email_body,
|
||||
];
|
||||
$recipients = [];
|
||||
if (filter_var($sla_notification_email, FILTER_VALIDATE_EMAIL)) {
|
||||
$recipients[] = ['email' => $sla_notification_email, 'greet' => '', 'footnote' => 'You get this at the SLA alert address in Admin > SLAs.'];
|
||||
}
|
||||
|
||||
// Assigned agent (skip a duplicate if they are also the notification address)
|
||||
if (!empty($ticket['user_email']) && strtolower($ticket['user_email']) != strtolower($sla_notification_email)) {
|
||||
$email_data[] = [
|
||||
'from' => $from_email,
|
||||
'from_name' => escapeSql($from_name),
|
||||
'recipient' => $ticket['user_email'],
|
||||
'recipient_name' => escapeSql(strval($ticket['user_name'])),
|
||||
'subject' => $email_subject,
|
||||
'body' => $email_body,
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($email_data)) {
|
||||
addToMailQueue($email_data);
|
||||
// The assigned agent - once, if they are also the alert address
|
||||
if ($agent = ticketMailAgent($row['ticket_assigned_to'])) {
|
||||
$recipients[] = $agent + ['footnote' => "You get this as the agent $ticket_ref is assigned to."];
|
||||
}
|
||||
ticketMailAgents($recipients, "$subject_line: $ticket_ref - " . $row['ticket_subject'],
|
||||
ticketAgentEmailContent($row, 'sla', ['track' => $track, 'breached' => $breached, 'due' => $due]));
|
||||
}
|
||||
|
||||
// --- Response SLA track ---
|
||||
@@ -164,13 +142,13 @@ while ($ticket = mysqli_fetch_assoc($sql_response)) {
|
||||
if ($now >= $due) {
|
||||
// Breached without a response - the verdict is final, record the miss
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_response_sla_alert_stage = 2, ticket_response_sla_met = 0 WHERE ticket_id = $ticket_id");
|
||||
sendSlaAlert($ticket, "Response SLA breached", "The response SLA on this ticket was missed (due {$ticket['ticket_response_due_at']}).");
|
||||
sendSlaAlert($ticket, 'response', true, $ticket['ticket_response_due_at']);
|
||||
|
||||
} elseif ($stage < 1 && $warning_percent) {
|
||||
$warn_at = strtotime(addBusinessMinutes($ticket['ticket_created_at'], floor(intval($ticket['sla_response_minutes']) * $warning_percent / 100)));
|
||||
if ($now >= $warn_at) {
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_response_sla_alert_stage = 1 WHERE ticket_id = $ticket_id");
|
||||
sendSlaAlert($ticket, "Response SLA at risk", "This ticket is approaching its response SLA (due {$ticket['ticket_response_due_at']}).");
|
||||
sendSlaAlert($ticket, 'response', false, $ticket['ticket_response_due_at']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -199,14 +177,14 @@ while ($ticket = mysqli_fetch_assoc($sql_resolution)) {
|
||||
|
||||
if ($now >= $due) {
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_resolution_sla_alert_stage = 2, ticket_resolution_sla_met = 0 WHERE ticket_id = $ticket_id");
|
||||
sendSlaAlert($ticket, "Resolution SLA breached", "The resolution SLA on this ticket was missed (due {$ticket['ticket_resolution_due_at']}).");
|
||||
sendSlaAlert($ticket, 'resolution', true, $ticket['ticket_resolution_due_at']);
|
||||
|
||||
} elseif ($stage < 1 && $warning_percent) {
|
||||
// Measured against consumed clock time, so paused spells don't warn early
|
||||
$warn_after_minutes = floor(intval($ticket['sla_resolution_minutes']) * $warning_percent / 100);
|
||||
if (getTicketSlaConsumedMinutes($ticket_id) >= $warn_after_minutes) {
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_resolution_sla_alert_stage = 1 WHERE ticket_id = $ticket_id");
|
||||
sendSlaAlert($ticket, "Resolution SLA at risk", "This ticket is approaching its resolution SLA (due {$ticket['ticket_resolution_due_at']}).");
|
||||
sendSlaAlert($ticket, 'resolution', false, $ticket['ticket_resolution_due_at']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,17 +170,29 @@ function emailHtml(array $email) {
|
||||
. '</td></tr></table></body></html>';
|
||||
}
|
||||
|
||||
/** The mail settings, read once, as saved - cron scripts load their own copies, or none. */
|
||||
function emailSettings() {
|
||||
global $mysqli;
|
||||
static $row = null;
|
||||
if ($row === null) {
|
||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT config_smtp_provider, config_mail_from_email, config_mail_from_name, config_ticket_from_email, config_ticket_from_name
|
||||
FROM settings WHERE company_id = 1")) ?: [];
|
||||
}
|
||||
return $row;
|
||||
}
|
||||
|
||||
/** Can mail go out - is a mail provider set up (Admin > Mail)? */
|
||||
function emailReady() {
|
||||
return !empty(emailSettings()['config_smtp_provider']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Who an email comes from, read from the settings as saved (cron scripts keep
|
||||
* SQL-escaped copies): system - the mailbox for everything else (Admin >
|
||||
* Mail), or ticket - the ticket mailbox. ['email' => ..., 'name' => ...].
|
||||
*/
|
||||
function emailFrom($which = 'system') {
|
||||
global $mysqli;
|
||||
static $row = null;
|
||||
if ($row === null) {
|
||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT config_mail_from_email, config_mail_from_name, config_ticket_from_email, config_ticket_from_name FROM settings WHERE company_id = 1")) ?: [];
|
||||
}
|
||||
$row = emailSettings();
|
||||
if ($which === 'ticket') {
|
||||
return ['email' => (string) ($row['config_ticket_from_email'] ?? ''), 'name' => (string) ($row['config_ticket_from_name'] ?? '')];
|
||||
}
|
||||
@@ -213,11 +225,12 @@ function emailMessageHtml($html) {
|
||||
* $email is emailHtml()'s, its values raw. $opts:
|
||||
* from ['email' => ..., 'name' => ...] - the system mailbox (emailFrom()) when not given
|
||||
* attachments [['path' => app-relative, 'name' => ...], ...]
|
||||
* cal_str a calendar file (iCalendar) to go with it - an invite or its cancellation
|
||||
* Who it was queued to, "Name (email)". Nothing is queued without mail set up.
|
||||
*/
|
||||
function emailQueue(array $recipients, $subject, array $email, array $opts = []) {
|
||||
global $mysqli, $config_smtp_provider;
|
||||
if (empty($config_smtp_provider)) {
|
||||
global $mysqli;
|
||||
if (!emailReady()) {
|
||||
return [];
|
||||
}
|
||||
$from = $opts['from'] ?? emailFrom();
|
||||
@@ -241,7 +254,7 @@ function emailQueue(array $recipients, $subject, array $email, array $opts = [])
|
||||
'subject' => mysqli_real_escape_string($mysqli, (string) $subject),
|
||||
'body' => mysqli_real_escape_string($mysqli, $html),
|
||||
'attachments' => $attachments,
|
||||
];
|
||||
] + (!empty($opts['cal_str']) ? ['cal_str' => (string) $opts['cal_str']] : []);
|
||||
$sent[strtolower($address)] = $name !== '' ? "$name ($address)" : $address;
|
||||
}
|
||||
if ($data) {
|
||||
|
||||
@@ -678,9 +678,15 @@ function shareNotifyCreator($row, $title, $details, $email = false, $always = fa
|
||||
}
|
||||
mysqli_query($mysqli, "INSERT INTO notifications SET notification_type = '" . escapeSql(mb_substr($title, 0, 200)) . "', notification = '" . escapeSql(mb_substr($details, 0, 1000)) . "',
|
||||
notification_action = '" . escapeSql($action) . "', notification_client_id = $client_id, notification_entity_id = " . intval($row['item_id']) . ", notification_user_id = $user_id");
|
||||
if ($email && shareMailReady() && filter_var($user['user_email'], FILTER_VALIDATE_EMAIL)) {
|
||||
if ($email && shareMailReady()) {
|
||||
global $config_base_url;
|
||||
shareQueueMail($user['user_email'], "$title - " . $row['client_name'], shareMailHtml(escapeHtml($title), [escapeHtml($details), 'Open it in Shared Links to save it to the client\'s credentials, or discard it.'], 'Review it', "https://$config_base_url$action"));
|
||||
emailQueue([['email' => $user['user_email'], 'name' => $user['user_name'], 'footnote' => 'You get this because you asked for this login with a shared link.']],
|
||||
"$title - " . $row['client_name'], [
|
||||
'kind' => 'Shared links', 'tone' => 'primary', 'preview' => $details,
|
||||
'title' => $title, 'intro' => [$details, "Open it in Shared Links to save it to the client's credentials, or discard it."],
|
||||
'summary' => ['Client' => (string) $row['client_name'], 'Asked for' => (string) shareName($row)],
|
||||
'button' => ['label' => 'Review it', 'url' => "https://$config_base_url$action"],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -345,6 +345,7 @@ function ticketEmailSla($ticket_id) {
|
||||
* resolved marked resolved, pending closure - message (the agent's note, HTML, or ''), by
|
||||
* closed closed - asking how we did
|
||||
* watcher they were made a watcher - by (who added them)
|
||||
* scheduled the work was booked - when (words), where (the site's address), or cancelled: true
|
||||
*/
|
||||
function ticketEmailContent(array $row, $kind, array $opts = []) {
|
||||
global $config_ticket_autoclose_hours;
|
||||
@@ -404,6 +405,15 @@ function ticketEmailContent(array $row, $kind, array $opts = []) {
|
||||
$email['summary'] = $summary;
|
||||
$email['button']['label'] = 'Tell us how we did';
|
||||
$email['after'] = ['Need more help with this? Raise a new ticket - replies to this email do not reach us.'];
|
||||
} elseif ($kind === 'scheduled') {
|
||||
$cancelled = !empty($opts['cancelled']);
|
||||
$email['kind'] = $cancelled ? 'Visit cancelled' : 'Work booked';
|
||||
$email['tone'] = $cancelled ? 'secondary' : 'primary';
|
||||
$email['title'] = $cancelled ? 'The booked work is cancelled' : 'Your ticket is booked in';
|
||||
$email['intro'] = [$cancelled
|
||||
? "The work booked on ticket $reference is cancelled - the calendar file with this email takes it out of your calendar. We will be in touch to book it again."
|
||||
: "We have booked the work on ticket $reference for " . $opts['when'] . ". The calendar file with this email adds it to your calendar."];
|
||||
$email['summary'] = $summary + ($cancelled ? [] : ['When' => (string) $opts['when'], 'Where' => !empty($row['ticket_onsite']) ? (trim((string) ($opts['where'] ?? '')) !== '' ? (string) $opts['where'] : 'Onsite') : 'Remote']);
|
||||
} elseif ($kind === 'watcher') {
|
||||
$email['kind'] = 'Watching';
|
||||
$email['title'] = 'You were added to a ticket';
|
||||
@@ -431,8 +441,8 @@ function ticketEmailContent(array $row, $kind, array $opts = []) {
|
||||
* (ticketColleagueRecipients())
|
||||
*/
|
||||
function ticketMailSend($ticket_id, $subject, array $email, array $opts = []) {
|
||||
global $mysqli, $config_smtp_provider;
|
||||
if (empty($config_smtp_provider)) {
|
||||
global $mysqli;
|
||||
if (!emailReady()) {
|
||||
return 0;
|
||||
}
|
||||
$ticket_id = intval($ticket_id);
|
||||
@@ -465,7 +475,7 @@ function ticketMailSend($ticket_id, $subject, array $email, array $opts = []) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return count(emailQueue($recipients, $subject, $email, ['from' => ticketEmailFrom(), 'attachments' => $opts['attachments'] ?? []]));
|
||||
return count(emailQueue($recipients, $subject, $email, ['from' => ticketEmailFrom(), 'attachments' => $opts['attachments'] ?? [], 'cal_str' => $opts['cal_str'] ?? '']));
|
||||
}
|
||||
|
||||
/** Whether a ticket's contact gets an email of this $kind - no 'created' or 'closed' email for one who asked for replies only. */
|
||||
@@ -511,8 +521,8 @@ function ticketColleagueRecipients($ticket_id, $kind) {
|
||||
* notifications have to be on (Admin > Ticket). How many were queued.
|
||||
*/
|
||||
function ticketMailColleaguesNew($ticket_id) {
|
||||
global $config_smtp_provider, $config_ticket_client_general_notifications, $config_base_url;
|
||||
if (empty($config_smtp_provider) || intval($config_ticket_client_general_notifications) !== 1) {
|
||||
global $config_ticket_client_general_notifications, $config_base_url;
|
||||
if (!emailReady() || intval($config_ticket_client_general_notifications) !== 1) {
|
||||
return 0;
|
||||
}
|
||||
$ticket = ticketRow($ticket_id);
|
||||
@@ -540,6 +550,170 @@ function ticketMailColleaguesNew($ticket_id) {
|
||||
return $sent;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ Emails about a ticket, to agents */
|
||||
|
||||
/** Where an agent opens a ticket - in ITFlow. */
|
||||
function ticketAgentUrl(array $row) {
|
||||
global $config_base_url;
|
||||
return "https://$config_base_url/agent/" . ticketUrl($row['ticket_id'], $row['ticket_client_id'] ?? 0);
|
||||
}
|
||||
|
||||
/** An agent to email - ['email', 'name'] of an active one, or null. */
|
||||
function ticketMailAgent($user_id) {
|
||||
global $mysqli;
|
||||
$user_id = intval($user_id);
|
||||
$row = $user_id ? mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT user_name, user_email FROM users
|
||||
WHERE user_id = $user_id AND user_type = 1 AND user_status = 1 AND user_archived_at IS NULL LIMIT 1")) : null;
|
||||
return $row && filter_var($row['user_email'], FILTER_VALIDATE_EMAIL) ? ['email' => $row['user_email'], 'name' => $row['user_name']] : null;
|
||||
}
|
||||
|
||||
/*
|
||||
* What an email to an agent about a ticket says - emailHtml()'s fields
|
||||
* (functions/email.php), all but who it is to and why. $row is ticketRow().
|
||||
* $kind and its $opts:
|
||||
* new it was raised - how: portal, email or scheduled
|
||||
* replied the client replied - how: portal or email, message (the reply, HTML), by (who)
|
||||
* assigned it was assigned to them - by (who)
|
||||
* sla an SLA alert - track: response or resolution, breached: true once missed, due (when)
|
||||
* scheduled the work was booked - when, where, or cancelled: true
|
||||
*/
|
||||
function ticketAgentEmailContent(array $row, $kind, array $opts = []) {
|
||||
$reference = ticketReference($row);
|
||||
$subject = (string) $row['ticket_subject'];
|
||||
$client = !empty($row['client_name']) ? (string) $row['client_name'] : (intval($row['ticket_client_id'] ?? 0) ? 'A client' : 'No client');
|
||||
$by = trim((string) ($opts['by'] ?? ''));
|
||||
$email = [
|
||||
'kind' => 'Ticket', 'tone' => 'primary', 'title' => $subject, 'preview' => "$reference - $subject",
|
||||
'summary' => ['Client' => $client, 'Ticket' => $reference, 'Priority' => (string) $row['ticket_priority'], 'Status' => (string) ($row['ticket_status_name'] ?? '')],
|
||||
'button' => ['label' => 'Open in ITFlow', 'url' => ticketAgentUrl($row)],
|
||||
];
|
||||
if ($kind === 'new') {
|
||||
$how = ['portal' => ' in the client portal', 'email' => ' by email', 'scheduled' => ', as scheduled'][$opts['how'] ?? ''] ?? '';
|
||||
$who = ($opts['how'] ?? '') === 'scheduled' ? 'A recurring ticket was raised' : (trim((string) ($row['contact_name'] ?? '')) !== '' ? $row['contact_name'] . ' raised a ticket' : 'A ticket was raised');
|
||||
$email['kind'] = 'New ticket';
|
||||
$email['intro'] = [($opts['how'] ?? '') === 'scheduled' ? "$who for $client, as scheduled." : "$who$how for $client."];
|
||||
$email['message'] = ['html' => (string) $row['ticket_details'], 'label' => 'Details'];
|
||||
$email['summary'] = ['Client' => $client, 'Ticket' => $reference, 'Priority' => (string) $row['ticket_priority']]
|
||||
+ (trim((string) ($row['contact_name'] ?? '')) !== '' ? ['Contact' => (string) $row['contact_name']] : []);
|
||||
} elseif ($kind === 'replied') {
|
||||
$how = ['portal' => ' in the client portal', 'email' => ' by email'][$opts['how'] ?? ''] ?? '';
|
||||
$email['kind'] = 'Client replied';
|
||||
$email['intro'] = [($by !== '' ? $by : 'The client') . " replied to $reference$how."];
|
||||
$email['message'] = ['html' => (string) ($opts['message'] ?? ''), 'label' => ($by !== '' ? $by : 'They') . ' wrote'];
|
||||
$email['after'] = ['Answer it in ITFlow, so the client gets your reply.'];
|
||||
} elseif ($kind === 'assigned') {
|
||||
$email['kind'] = 'Assigned to you';
|
||||
$email['intro'] = [($by !== '' ? $by : 'Someone') . " assigned $reference to you."];
|
||||
$email['message'] = ['html' => (string) $row['ticket_details'], 'label' => 'Details'];
|
||||
} elseif ($kind === 'scheduled') {
|
||||
$cancelled = !empty($opts['cancelled']);
|
||||
$email['kind'] = $cancelled ? 'Visit cancelled' : 'Work booked';
|
||||
$email['tone'] = $cancelled ? 'secondary' : 'primary';
|
||||
$email['intro'] = [$cancelled ? "The work booked on $reference is cancelled - the calendar file with this email takes it out of your calendar." : "$reference is booked for " . $opts['when'] . " - the calendar file with this email adds it to your calendar."];
|
||||
if (!$cancelled) {
|
||||
$email['summary'] += ['When' => (string) $opts['when'], 'Where' => !empty($row['ticket_onsite']) ? (trim((string) ($opts['where'] ?? '')) !== '' ? (string) $opts['where'] : 'Onsite') : 'Remote'];
|
||||
}
|
||||
} elseif ($kind === 'sla') {
|
||||
$track = ($opts['track'] ?? 'response') === 'resolution' ? 'resolution' : 'response';
|
||||
$due = !empty($opts['due']) ? date('D j M, g:i A', strtotime($opts['due'])) : '';
|
||||
$breached = !empty($opts['breached']);
|
||||
$email['kind'] = ucfirst($track) . ($breached ? ' SLA missed' : ' SLA at risk');
|
||||
$email['tone'] = $breached ? 'danger' : 'warning';
|
||||
$email['intro'] = [$breached
|
||||
? "The $track due on $reference by $due was missed."
|
||||
: "$reference is close to its $track SLA - it is due by $due."];
|
||||
$email['summary'] += ['Assigned' => !empty($row['user_name']) ? (string) $row['user_name'] : 'Nobody', ucfirst($track) . ' due' => $due];
|
||||
}
|
||||
return $email;
|
||||
}
|
||||
|
||||
/** Queue an email to agents (ticketAgentEmailContent()) from the ticket mailbox - [['email', 'name', 'footnote'], ...]. How many went. */
|
||||
function ticketMailAgents(array $recipients, $subject, array $email) {
|
||||
return count(emailQueue($recipients, $subject, $email, ['from' => ticketEmailFrom()]));
|
||||
}
|
||||
|
||||
/*
|
||||
* Tell the new-ticket address (Admin > Ticket) a ticket was raised - $how:
|
||||
* portal, email or scheduled. Nothing when it isn't set.
|
||||
*/
|
||||
function ticketMailNewTicketAddress($ticket_id, $how) {
|
||||
global $mysqli, $config_app_name;
|
||||
$address = (string) (mysqli_fetch_row(mysqli_query($mysqli, "SELECT config_ticket_new_ticket_notification_email FROM settings WHERE company_id = 1"))[0] ?? '');
|
||||
$row = ticketRow($ticket_id);
|
||||
if (!filter_var($address, FILTER_VALIDATE_EMAIL) || !$row) {
|
||||
return 0;
|
||||
}
|
||||
$client = !empty($row['client_name']) ? $row['client_name'] : 'Guest';
|
||||
$subject = ($config_app_name ?: 'ITFlow') . ($how === 'scheduled' ? ' - New Recurring Ticket - ' : ' - New Ticket - ') . "$client: " . $row['ticket_subject'];
|
||||
return ticketMailAgents([['email' => $address, 'greet' => '', 'footnote' => 'You get this at the new-ticket address in Admin > Ticket.']],
|
||||
$subject, ticketAgentEmailContent($row, 'new', ['how' => $how]));
|
||||
}
|
||||
|
||||
/** Tell the agent a ticket is assigned to that the client replied - $how portal or email, $reply its HTML, $by who. */
|
||||
function ticketMailAssigneeReplied($ticket_id, $how, $reply, $by) {
|
||||
global $config_app_name;
|
||||
$row = ticketRow($ticket_id);
|
||||
$agent = $row ? ticketMailAgent($row['ticket_assigned_to']) : null;
|
||||
if (!$agent) {
|
||||
return 0;
|
||||
}
|
||||
return ticketMailAgents([$agent + ['footnote' => 'You get this as the agent ' . ticketReference($row) . ' is assigned to.']],
|
||||
($config_app_name ?: 'ITFlow') . " - Ticket updated - [" . ticketReference($row) . "] " . $row['ticket_subject'],
|
||||
ticketAgentEmailContent($row, 'replied', ['how' => $how, 'message' => (string) $reply, 'by' => (string) $by]));
|
||||
}
|
||||
|
||||
/*
|
||||
* Tell an agent tickets are theirs now - one email for any number. Not when
|
||||
* they assigned them themselves. $ticket_ids, $by raw. How many went.
|
||||
*/
|
||||
function ticketMailAssigned($user_id, array $ticket_ids, $by) {
|
||||
global $session_user_id, $config_app_name, $config_base_url;
|
||||
$agent = intval($user_id) !== intval($session_user_id ?? 0) ? ticketMailAgent($user_id) : null;
|
||||
$rows = array_values(array_filter(array_map('ticketRow', array_unique(array_map('intval', $ticket_ids)))));
|
||||
if (!$agent || !$rows) {
|
||||
return 0;
|
||||
}
|
||||
$app = $config_app_name ?: 'ITFlow';
|
||||
$agent['footnote'] = 'You get this because ' . (count($rows) === 1 ? 'the ticket was' : 'they were') . ' assigned to you in ITFlow.';
|
||||
if (count($rows) === 1) {
|
||||
$row = $rows[0];
|
||||
return ticketMailAgents([$agent], "$app - Ticket " . ticketReference($row) . " assigned to you - " . $row['ticket_subject'], ticketAgentEmailContent($row, 'assigned', ['by' => $by]));
|
||||
}
|
||||
$lines = [];
|
||||
foreach ($rows as $row) {
|
||||
$lines[] = [['text' => ticketReference($row), 'url' => ticketAgentUrl($row)], (string) $row['ticket_subject'], !empty($row['client_name']) ? (string) $row['client_name'] : '-', (string) $row['ticket_priority']];
|
||||
}
|
||||
return ticketMailAgents([$agent], "$app - " . count($rows) . " tickets have been assigned to you", [
|
||||
'kind' => 'Assigned to you', 'tone' => 'primary', 'preview' => "$by assigned " . count($rows) . ' tickets to you',
|
||||
'title' => count($rows) . ' tickets are yours',
|
||||
'intro' => ["$by assigned these tickets to you."],
|
||||
'lines' => ['head' => ['Ticket', 'Subject', 'Client', 'Priority'], 'rows' => $lines],
|
||||
'button' => ['label' => 'Open your tickets', 'url' => "https://$config_base_url/agent/tickets.php?assigned=" . intval($user_id)],
|
||||
]);
|
||||
}
|
||||
|
||||
/*
|
||||
* Tell an agent recurring tickets are theirs now - the tickets they raise
|
||||
* will be. $subjects (raw) of the recurring tickets, $by who did it. Not when
|
||||
* they did it themselves.
|
||||
*/
|
||||
function recurringTicketMailAssigned($user_id, array $subjects, $by) {
|
||||
global $session_user_id, $config_app_name, $config_base_url;
|
||||
$agent = intval($user_id) !== intval($session_user_id ?? 0) ? ticketMailAgent($user_id) : null;
|
||||
if (!$agent || !$subjects) {
|
||||
return 0;
|
||||
}
|
||||
$count = count($subjects);
|
||||
$agent['footnote'] = 'You get this because ' . ($count === 1 ? 'it was' : 'they were') . ' assigned to you in ITFlow.';
|
||||
return ticketMailAgents([$agent], ($config_app_name ?: 'ITFlow') . ($count === 1 ? " - Recurring ticket " . $subjects[0] . " assigned to you" : " - $count recurring tickets assigned to you"), [
|
||||
'kind' => 'Assigned to you', 'tone' => 'primary', 'preview' => "$by assigned recurring tickets to you",
|
||||
'title' => $count === 1 ? 'A recurring ticket is yours' : "$count recurring tickets are yours",
|
||||
'intro' => ["$by assigned " . ($count === 1 ? 'this recurring ticket' : 'these recurring tickets') . " to you - the tickets " . ($count === 1 ? 'it raises' : 'they raise') . " will be yours."],
|
||||
'lines' => ['head' => ['Recurring ticket'], 'rows' => array_map(function ($subject) { return [(string) $subject]; }, array_values($subjects))],
|
||||
'button' => ['label' => 'Open Recurring Tickets', 'url' => "https://$config_base_url/agent/recurring_tickets.php?assigned_agent=" . intval($user_id)],
|
||||
]);
|
||||
}
|
||||
|
||||
/** Time worked as posted - HH:MM:SS, or H:MM - normalised to HH:MM:SS; nothing sensible is 00:00:00. */
|
||||
function ticketTimeWorked($input) {
|
||||
if (preg_match('/^\s*(\d{1,3}):([0-5]?\d)(?::([0-5]?\d))?\s*$/', (string) $input, $match)) {
|
||||
@@ -1079,7 +1253,7 @@ function recurringTicketNextRunPill($row) {
|
||||
* log. Does not move the next run - the caller does. The new ticket's id, or 0.
|
||||
*/
|
||||
function recurringTicketRaise($recurring_ticket_id, $by = 'Cron') {
|
||||
global $mysqli, $config_base_url, $config_ticket_client_general_notifications, $config_ticket_new_ticket_notification_email, $config_ticket_from_email, $config_ticket_from_name;
|
||||
global $mysqli, $config_ticket_client_general_notifications;
|
||||
|
||||
$row = recurringTicketRow($recurring_ticket_id);
|
||||
if (!$row) {
|
||||
@@ -1143,26 +1317,8 @@ function recurringTicketRaise($recurring_ticket_id, $by = 'Cron') {
|
||||
ticketEmailContent($new, 'created', ['how' => 'scheduled']), ['kind' => 'created', 'to_watchers' => false]);
|
||||
}
|
||||
|
||||
// The new-ticket address
|
||||
$client_name = escapeSql($new['client_name'] ?? '');
|
||||
$from_email = escapeSql($config_ticket_from_email);
|
||||
$from_name = escapeSql($config_ticket_from_name);
|
||||
$base_url = escapeSql($config_base_url);
|
||||
$data = [];
|
||||
if (filter_var($config_ticket_new_ticket_notification_email, FILTER_VALIDATE_EMAIL)) {
|
||||
$client_uri = $client_id ? "&client_id=$client_id" : '';
|
||||
$data[] = [
|
||||
'from' => $from_email,
|
||||
'from_name' => $from_name,
|
||||
'recipient' => escapeSql($config_ticket_new_ticket_notification_email),
|
||||
'recipient_name' => $from_name,
|
||||
'subject' => "ITFlow - New Recurring Ticket - $client_name: $subject",
|
||||
'body' => "Hello, <br><br>This is a notification that a recurring (scheduled) ticket has been raised in ITFlow. <br>Ticket: $prefix$ticket_number<br>Client: $client_name<br>Priority: $priority<br>Link: https://$base_url/agent/ticket.php?ticket_id=$ticket_id$client_uri <br><br>--------------------------------<br><br><b>$subject</b><br>$details",
|
||||
];
|
||||
}
|
||||
if ($data) {
|
||||
addToMailQueue($data);
|
||||
}
|
||||
// The new-ticket address (Admin > Ticket), when there is one
|
||||
ticketMailNewTicketAddress($ticket_id, 'scheduled');
|
||||
|
||||
return $ticket_id;
|
||||
}
|
||||
@@ -1279,59 +1435,27 @@ function ticketScheduleValue($input) {
|
||||
return $sql === 'NULL' ? '' : trim($sql, "'");
|
||||
}
|
||||
|
||||
/** The scheduled-visit emails - the agent, the contact and the watchers - with the calendar invite (or its cancellation). */
|
||||
/*
|
||||
* The scheduled-work emails, each with the calendar file - the invite, or its
|
||||
* cancellation: to the agent doing the work, and when client notifications
|
||||
* are on (Admin > Ticket) to the contact and the watchers. $row has the
|
||||
* ticket's id and, for a booking, location_address.
|
||||
*/
|
||||
function ticketMailSchedule($row, $cal_str, $cancelled, $when_text) {
|
||||
global $mysqli, $config_smtp_provider, $config_ticket_from_email, $config_ticket_from_name, $config_base_url, $config_ticket_client_general_notifications, $session_company_name;
|
||||
if (empty($config_smtp_provider)) {
|
||||
global $config_ticket_client_general_notifications;
|
||||
$ticket = ticketRow($row['ticket_id']);
|
||||
if (!$ticket || !emailReady()) {
|
||||
return;
|
||||
}
|
||||
// Built as plain text and HTML, then escaped once for the mail queue
|
||||
$sql = function ($value) use ($mysqli) {
|
||||
return mysqli_real_escape_string($mysqli, (string) $value);
|
||||
};
|
||||
$html = function ($value) {
|
||||
return escapeHtml((string) $value);
|
||||
};
|
||||
$ticket_id = intval($row['ticket_id']);
|
||||
$client_id = intval($row['ticket_client_id']);
|
||||
$reference = ticketReference($row);
|
||||
$subject = (string) $row['ticket_subject'];
|
||||
$company_name = (string) $session_company_name;
|
||||
$from_email = (string) $config_ticket_from_email;
|
||||
$agent_link = "https://$config_base_url/agent/ticket.php?ticket_id=$ticket_id" . ($client_id ? "&client_id=$client_id" : '');
|
||||
$client_link = "https://$config_base_url/client/ticket.php?id=$ticket_id";
|
||||
$what = $cancelled ? 'Scheduled work for the ticket regarding ' . $html($subject) . ' has been cancelled.' : 'The ticket regarding ' . $html($subject) . ' has been scheduled for ' . $html($when_text) . '.';
|
||||
$what_yours = $cancelled ? 'Scheduled work for your ticket regarding ' . $html($subject) . ' has been cancelled.' : 'Your ticket regarding ' . $html($subject) . ' has been scheduled for ' . $html($when_text) . '.';
|
||||
$mail_subject = $sql(($cancelled ? 'Ticket Schedule Cancelled' : 'Ticket Scheduled') . " - [$reference] - $subject");
|
||||
$footer = "<div class='footer'>~<br>" . $html($company_name) . "<br>Support Department<br>" . $html($from_email) . "<br></div><div class='no-reply'>This is an automated message. Please do not reply directly to this email.</div>";
|
||||
$from = ['from' => $sql($from_email), 'from_name' => $sql($config_ticket_from_name), 'subject' => $mail_subject, 'cal_str' => $cal_str];
|
||||
$data = [];
|
||||
|
||||
// The agent doing the work
|
||||
if (filter_var($row['user_email'] ?? '', FILTER_VALIDATE_EMAIL)) {
|
||||
$data[] = $from + ['recipient' => $sql($row['user_email']), 'recipient_name' => $sql($row['user_name']),
|
||||
'body' => $sql('Hello, ' . $html($row['user_name']) . "<br><br>$what<br><br>--------------------------------<br><a href=\"$agent_link\">" . $html($reference) . "</a><br>--------------------------------<br><br>Please do not reply to this email. <br><br>Ticket: " . $html($reference) . "<br>Subject: " . $html($subject) . "<br>Portal: $agent_link<br><br>~<br>" . $html($company_name) . "<br>Support Department<br>" . $html($from_email))];
|
||||
$reference = ticketReference($ticket);
|
||||
$opts = ['cancelled' => $cancelled, 'when' => $when_text, 'where' => (string) ($row['location_address'] ?? '')];
|
||||
$subject = ($cancelled ? 'Ticket Schedule Cancelled' : 'Ticket Scheduled') . " - [$reference] - " . $ticket['ticket_subject'];
|
||||
if ($agent = ticketMailAgent($ticket['ticket_assigned_to'])) {
|
||||
emailQueue([$agent + ['footnote' => "You get this as the agent $reference is assigned to."]], $subject,
|
||||
ticketAgentEmailContent($ticket, 'scheduled', $opts), ['from' => ticketEmailFrom(), 'cal_str' => $cal_str]);
|
||||
}
|
||||
|
||||
if (intval($config_ticket_client_general_notifications) === 1) {
|
||||
// The contact
|
||||
if (filter_var($row['contact_email'] ?? '', FILTER_VALIDATE_EMAIL)) {
|
||||
$data[] = $from + ['recipient' => $sql($row['contact_email']), 'recipient_name' => $sql($row['contact_name']),
|
||||
'body' => $sql("<div class='header'>Hello, " . $html($row['contact_name']) . "</div>$what_yours<br><br><a href='$client_link' class='link-button'>Access your ticket here</a><br><br>Please do not reply to this email.<br><br><strong>Ticket:</strong> " . $html($reference) . "<br><strong>Subject:</strong> " . $html($subject) . "<br><br><br>$footer")];
|
||||
}
|
||||
// And the watchers
|
||||
$watchers = mysqli_query($mysqli, "SELECT watcher_email FROM ticket_watchers WHERE watcher_ticket_id = $ticket_id");
|
||||
foreach (ticketWatcherEmails(array_column(mysqli_fetch_all($watchers, MYSQLI_ASSOC), 'watcher_email')) as $watcher_email) {
|
||||
if (strcasecmp($watcher_email, (string) ($row['contact_email'] ?? '')) === 0) {
|
||||
continue;
|
||||
}
|
||||
$data[] = $from + ['recipient' => $sql($watcher_email), 'recipient_name' => $sql($watcher_email),
|
||||
'body' => $sql("<div class='header'>Hello,</div>$what<br><br><a href='$client_link' class='link-button'>" . $html($reference) . "</a><br><br>Please do not reply to this email.<br><br><strong>Ticket:</strong> " . $html($reference) . "<br><strong>Subject:</strong> " . $html($subject) . "<br><strong>Portal:</strong> <a href='$client_link'>Access the ticket here</a><br><br>$footer")];
|
||||
}
|
||||
}
|
||||
|
||||
if ($data) {
|
||||
addToMailQueue($data);
|
||||
ticketMailSend($ticket['ticket_id'], $subject, ticketEmailContent($ticket, 'scheduled', $opts), ['cal_str' => $cal_str]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -79,8 +79,7 @@ function ticketChangePriority($ticket_id, $priority) {
|
||||
* becomes Open. Returns [ok, message].
|
||||
*/
|
||||
function ticketChangeAssignee($ticket_id, $assigned_to) {
|
||||
global $mysqli, $session_name, $session_user_id, $session_company_name,
|
||||
$config_smtp_provider, $config_ticket_from_name, $config_ticket_from_email, $config_app_name, $config_base_url;
|
||||
global $mysqli, $session_name, $session_user_id;
|
||||
|
||||
$ticket_id = intval($ticket_id);
|
||||
$assigned_to = intval($assigned_to);
|
||||
@@ -106,7 +105,6 @@ function ticketChangeAssignee($ticket_id, $assigned_to) {
|
||||
return [false, 'Invalid agent!'];
|
||||
}
|
||||
$agent_name = escapeSql($agent_details['user_name']);
|
||||
$agent_email = escapeSql($agent_details['user_email']);
|
||||
$ticket_reply = "Ticket re-assigned to $agent_name.";
|
||||
}
|
||||
|
||||
@@ -114,7 +112,6 @@ function ticketChangeAssignee($ticket_id, $assigned_to) {
|
||||
$ticket_number = intval($row['ticket_number']);
|
||||
$ticket_subject = escapeSql($row['ticket_subject']);
|
||||
$client_id = intval($row['ticket_client_id']);
|
||||
$client_name = escapeSql($row['client_name']);
|
||||
$client_uri = $client_id ? "&client_id=$client_id" : '';
|
||||
|
||||
// Update ticket & insert reply
|
||||
@@ -134,27 +131,7 @@ function ticketChangeAssignee($ticket_id, $assigned_to) {
|
||||
mysqli_query($mysqli, "INSERT INTO notifications SET notification_type = 'Ticket', notification = 'Ticket $ticket_prefix$ticket_number - Subject: $ticket_subject has been assigned to you by $session_name', notification_action = '/agent/ticket.php?ticket_id=$ticket_id$client_uri', notification_client_id = $client_id, notification_user_id = $assigned_to");
|
||||
|
||||
// Email Notification
|
||||
if (!empty($config_smtp_provider)) {
|
||||
|
||||
// Sanitize Config vars from get_settings.php
|
||||
$from_name = escapeSql($config_ticket_from_name);
|
||||
$from_email = escapeSql($config_ticket_from_email);
|
||||
$company_name = escapeSql($session_company_name);
|
||||
|
||||
$subject = "$config_app_name - Ticket $ticket_prefix$ticket_number assigned to you - $ticket_subject";
|
||||
$body = "Hi $agent_name, <br><br>A ticket has been assigned to you!<br><br>Client: $client_name<br>Ticket Number: $ticket_prefix$ticket_number<br> Subject: $ticket_subject<br><br>https://$config_base_url/agent/ticket.php?ticket_id=$ticket_id$client_uri <br><br>Thanks, <br>$session_name<br>$company_name";
|
||||
|
||||
addToMailQueue([
|
||||
[
|
||||
'from' => $from_email,
|
||||
'from_name' => $from_name,
|
||||
'recipient' => $agent_email,
|
||||
'recipient_name' => $agent_name,
|
||||
'subject' => $subject,
|
||||
'body' => $body,
|
||||
]
|
||||
]);
|
||||
}
|
||||
ticketMailAssigned($assigned_to, [$ticket_id], stripslashes($session_name));
|
||||
}
|
||||
|
||||
triggerCustomAction('ticket_assign', $ticket_id);
|
||||
@@ -364,8 +341,7 @@ function projectChangeManager($project_id, $manager_id) {
|
||||
* client access.
|
||||
*/
|
||||
function recurringTicketChangeAssignee($recurring_ticket_id, $assigned_to) {
|
||||
global $mysqli, $session_name, $session_user_id, $session_company_name,
|
||||
$config_smtp_provider, $config_ticket_from_name, $config_ticket_from_email, $config_app_name;
|
||||
global $mysqli, $session_name, $session_user_id;
|
||||
|
||||
$recurring_ticket_id = intval($recurring_ticket_id);
|
||||
$assigned_to = intval($assigned_to);
|
||||
@@ -380,7 +356,6 @@ function recurringTicketChangeAssignee($recurring_ticket_id, $assigned_to) {
|
||||
return [false, 'Invalid agent!'];
|
||||
}
|
||||
$agent_name = escapeSql($agent['user_name']);
|
||||
$agent_email = escapeSql($agent['user_email']);
|
||||
} else {
|
||||
$agent_name = 'No One';
|
||||
}
|
||||
@@ -395,19 +370,7 @@ function recurringTicketChangeAssignee($recurring_ticket_id, $assigned_to) {
|
||||
if ($assigned_to && $assigned_to != $session_user_id) {
|
||||
mysqli_query($mysqli, "INSERT INTO notifications SET notification_type = 'Recurring Ticket', notification = 'Recurring ticket $subject has been assigned to you by $session_name', notification_action = 'recurring_tickets.php?assigned=$assigned_to', notification_client_id = $client_id, notification_user_id = $assigned_to");
|
||||
|
||||
if (!empty($config_smtp_provider)) {
|
||||
$company_name = escapeSql($session_company_name);
|
||||
addToMailQueue([
|
||||
[
|
||||
'from' => escapeSql($config_ticket_from_email),
|
||||
'from_name' => escapeSql($config_ticket_from_name),
|
||||
'recipient' => $agent_email,
|
||||
'recipient_name' => $agent_name,
|
||||
'subject' => "$config_app_name - Recurring ticket $subject assigned to you",
|
||||
'body' => "Hi $agent_name, <br><br>$session_name assigned the recurring ticket $subject to you - the tickets it opens will be yours.<br><br>Thanks, <br>$session_name<br>$company_name",
|
||||
]
|
||||
]);
|
||||
}
|
||||
recurringTicketMailAssigned($assigned_to, [(string) $row['recurring_ticket_subject']], stripslashes($session_name));
|
||||
}
|
||||
|
||||
return [true, $assigned_to ? "Recurring ticket assigned to $agent_name" : 'Recurring ticket unassigned'];
|
||||
|
||||
@@ -475,35 +475,72 @@ function userHandOverWork($user_id, $to_user_id, $why) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue an email to an agent from the system mailbox. addToMailQueue() takes
|
||||
* values ready for SQL, so they are escaped here - the body with
|
||||
* mysqli_real_escape_string(), not escapeSql(), which would strip its HTML.
|
||||
* True when queued, or the error.
|
||||
*/
|
||||
function userQueueMail($recipient, $recipient_name, $subject, $body_html) {
|
||||
global $mysqli, $config_mail_from_email, $config_mail_from_name;
|
||||
$escape = function ($value) use ($mysqli) {
|
||||
return mysqli_real_escape_string($mysqli, (string) $value);
|
||||
};
|
||||
return addToMailQueue([[
|
||||
'from' => $escape($config_mail_from_email),
|
||||
'from_name' => $escape($config_mail_from_name),
|
||||
'recipient' => $escape($recipient),
|
||||
'recipient_name' => $escape($recipient_name),
|
||||
'subject' => $escape($subject),
|
||||
'body' => $escape($body_html),
|
||||
]]);
|
||||
/* ------------------------------------------------------------ Emails to an agent about their account */
|
||||
|
||||
/** Where an agent signs in - with the login key when one is required. */
|
||||
function userSignInUrl() {
|
||||
global $mysqli, $config_base_url;
|
||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT config_login_key_required, config_login_key_secret FROM settings WHERE company_id = 1")) ?: [];
|
||||
return "https://$config_base_url/login.php" . (!empty($row['config_login_key_required']) ? '?key=' . rawurlencode((string) $row['config_login_key_secret']) : '');
|
||||
}
|
||||
|
||||
/** An agent's own email, sent from the system - their account was changed. Queued; nothing when mail is not set up. */
|
||||
function userNotifyChange($name, $email, $what) {
|
||||
global $config_smtp_host, $config_app_name, $session_company_name;
|
||||
if (empty($config_smtp_host) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
return;
|
||||
}
|
||||
$app = $config_app_name ?: 'ITFlow';
|
||||
userQueueMail($email, $name, "$app account update for $name",
|
||||
'Hi ' . escapeHtml($name) . ',<br><br>Your ' . escapeHtml($app) . ' account was changed: <b>' . escapeHtml($what) . '</b><br><br>'
|
||||
. 'If this was not you, contact your ' . escapeHtml($app) . ' administrator straight away.<br><br>Thanks,<br>' . escapeHtml($session_company_name ?? $app));
|
||||
/** The app's name for these emails - Admin > Settings, ITFlow by default. */
|
||||
function userMailAppName() {
|
||||
global $config_app_name;
|
||||
return trim((string) ($config_app_name ?? '')) !== '' ? (string) $config_app_name : 'ITFlow';
|
||||
}
|
||||
|
||||
/*
|
||||
* A new agent's sign in details - their email, the password as it was set
|
||||
* (theirs to change) and where to sign in. True when queued.
|
||||
*/
|
||||
function userMailNewAccount($name, $email, $password, $force_mfa) {
|
||||
$company_name = (string) (billingCompany()['company_name'] ?? '');
|
||||
$app = userMailAppName();
|
||||
return (bool) emailQueue([['email' => $email, 'name' => $name, 'footnote' => "You get this because an account was set up for you in $company_name's $app."]],
|
||||
"Your new $company_name $app account", [
|
||||
'kind' => 'Your account', 'tone' => 'primary', 'preview' => "Your $app sign in details",
|
||||
'title' => "Your $app account is ready",
|
||||
'intro' => ["An $app account has been set up for you. Sign in with these details, then change your password."
|
||||
. ($force_mfa ? ' Have an authenticator app ready - you will set up multi-factor authentication the first time.' : '')],
|
||||
'summary' => ['Email' => (string) $email, 'Password' => (string) $password],
|
||||
'button' => ['label' => 'Sign in', 'url' => userSignInUrl()],
|
||||
'after' => ['This email has your password in it - delete it once you have changed it.'],
|
||||
]);
|
||||
}
|
||||
|
||||
/** An agent's own email, sent from the system - their account was changed ($what, raw). Queued; nothing when mail is not set up. */
|
||||
function userNotifyChange($name, $email, $what) {
|
||||
$app = userMailAppName();
|
||||
emailQueue([['email' => $email, 'name' => $name, 'footnote' => "You get this about your own $app account."]], "$app account update for $name", [
|
||||
'kind' => 'Your account', 'tone' => 'warning', 'preview' => "Your $app account was changed",
|
||||
'title' => 'Your account was changed',
|
||||
'intro' => ["Your $app account was changed: " . $what . '.'],
|
||||
'callout' => "If this was not you, tell your $app administrator straight away.",
|
||||
'button' => ['label' => "Sign in to $app", 'url' => userSignInUrl()],
|
||||
]);
|
||||
}
|
||||
|
||||
/*
|
||||
* Tell an agent about a sign in to their account - $kind new (a device and
|
||||
* address they hadn't signed in from) or mfa (the right password, a wrong
|
||||
* code). Raw values in.
|
||||
*/
|
||||
function userMailSignInAlert($name, $email, $kind, $ip, $user_agent) {
|
||||
global $config_base_url;
|
||||
$app = userMailAppName();
|
||||
$mfa = $kind === 'mfa';
|
||||
$device = trim(strip_tags(userDeviceHtml((string) $user_agent)));
|
||||
emailQueue([['email' => $email, 'name' => $name, 'footnote' => "You get this about your own $app account."]],
|
||||
$mfa ? "Important: $app failed 2FA login attempt for $name" : "$app new login for $name", [
|
||||
'kind' => 'Security', 'tone' => $mfa ? 'danger' : 'warning',
|
||||
'preview' => $mfa ? 'Your password was used, but the 2FA code was wrong' : 'A sign in from a new device',
|
||||
'title' => $mfa ? 'Someone used your password' : 'A new sign in to your account',
|
||||
'intro' => [$mfa
|
||||
? "Someone signed in to your $app account with your password, but the multi-factor code was wrong, so they were stopped."
|
||||
: "Someone signed in to your $app account from a device and address it hasn't seen before. If that was you, there is nothing to do."],
|
||||
'summary' => ['When' => date('D j M Y, g:i A'), 'Device' => $device !== '' ? $device : 'Unknown device', 'IP address' => (string) $ip],
|
||||
'callout' => $mfa ? 'If that was not you, your password is known to someone else - change it now.' : 'If that was not you, change your password now and tell your administrator.',
|
||||
'button' => ['label' => 'Review your sign in security', 'url' => "https://$config_base_url/agent/user/user_security.php"],
|
||||
]);
|
||||
}
|
||||
|
||||
29
login.php
29
login.php
@@ -452,19 +452,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_
|
||||
"));
|
||||
$ua_prev_logins = escapeSql($sql_ua_prev_logins['ua_previous_logins']);
|
||||
|
||||
if (!empty($config_smtp_provider) && $ip_previous_logins == 0 && $ua_prev_logins == 0) {
|
||||
$subject = "$config_app_name new login for $user_name";
|
||||
$body = "Hi $user_name, <br><br>A recent successful login to your $config_app_name account was considered a little unusual. If this was you, you can safely ignore this email!<br><br>IP Address: $session_ip<br> User Agent: $session_user_agent <br><br>If you did not perform this login, your credentials may be compromised. <br><br>Thanks, <br>ITFlow";
|
||||
|
||||
$data = [[
|
||||
'from' => $config_mail_from_email,
|
||||
'from_name' => $config_mail_from_name,
|
||||
'recipient' => $user_email,
|
||||
'recipient_name' => $user_name,
|
||||
'subject' => $subject,
|
||||
'body' => $body
|
||||
]];
|
||||
addToMailQueue($data);
|
||||
if ($ip_previous_logins == 0 && $ua_prev_logins == 0) {
|
||||
userMailSignInAlert($selectedRow['user_name'], $selectedRow['user_email'], 'new', getIP(), $_SERVER['HTTP_USER_AGENT'] ?? '');
|
||||
}
|
||||
|
||||
// Option B: set session_user_id BEFORE logAudit()
|
||||
@@ -587,19 +576,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_
|
||||
$session_user_id = $user_id;
|
||||
logAudit("Login", "MFA Failed", "$user_email failed MFA", 0, $user_id);
|
||||
|
||||
if (!empty($config_smtp_provider)) {
|
||||
$subject = "Important: $config_app_name failed 2FA login attempt for $user_name";
|
||||
$body = "Hi $user_name, <br><br>A recent login to your $config_app_name account was unsuccessful due to an incorrect 2FA code. If you did not attempt this login, your credentials may be compromised. <br><br>Thanks, <br>ITFlow";
|
||||
$data = [[
|
||||
'from' => $config_mail_from_email,
|
||||
'from_name' => $config_mail_from_name,
|
||||
'recipient' => $user_email,
|
||||
'recipient_name' => $user_name,
|
||||
'subject' => $subject,
|
||||
'body' => $body
|
||||
]];
|
||||
addToMailQueue($data);
|
||||
}
|
||||
userMailSignInAlert($selectedRow['user_name'], $selectedRow['user_email'], 'mfa', getIP(), $_SERVER['HTTP_USER_AGENT'] ?? '');
|
||||
|
||||
$response = "
|
||||
<div class='alert alert-danger'>
|
||||
|
||||
Reference in New Issue
Block a user