Mail > Previews * (admin/settings_mail.php shows them, admin/mail_preview.php draws one). * * Each preview is made by the code that sends the email, so it is what goes * out: it runs on a set of sample records (emailPreviewSample()) inside a * transaction that is then rolled back, while emailQueue() keeps the emails * instead of queuing them (emailCaptureStart(), functions/email.php). Custom * actions don't run meanwhile (triggerCustomAction()), and nothing that * writes files is called - an invoice's PDF is described, not made. */ /** The sample people the previews use - who they are and their addresses. */ const EMAIL_PREVIEW_PEOPLE = [ 'client' => 'Riverside Dental Practice', 'contact' => ['Alex Morgan', 'alex.morgan@example.com', 'Practice Manager'], 'colleague' => ['Jamie Chen', 'jamie.chen@example.com', 'IT Lead'], 'agent' => ['Sam Taylor', 'sam.taylor@example.com'], 'newcomer' => ['Riley Park', 'riley.park@example.com'], ]; /** * Make the sample records - inside the preview's transaction, so they are * gone again after. The team's own addresses that are not set (new tickets, * SLA alerts, quote and payment notices) are filled in for the moment, so * every email has someone to go to. Their ids and the values the previews use. */ function emailPreviewSample() { global $mysqli, $session_user_id; $insert = function ($sql) use ($mysqli) { mysqli_query($mysqli, $sql); return intval(mysqli_insert_id($mysqli)); }; $e = function ($value) use ($mysqli) { return mysqli_real_escape_string($mysqli, (string) $value); }; $people = EMAIL_PREVIEW_PEOPLE; $me = intval($session_user_id); mysqli_query($mysqli, "UPDATE settings SET config_ticket_new_ticket_notification_email = IF(COALESCE(config_ticket_new_ticket_notification_email, '') = '', 'tickets@example.com', config_ticket_new_ticket_notification_email), config_sla_notification_email = IF(COALESCE(config_sla_notification_email, '') = '', 'sla@example.com', config_sla_notification_email), config_quote_notification_email = IF(COALESCE(config_quote_notification_email, '') = '', 'quotes@example.com', config_quote_notification_email), config_invoice_paid_notification_email = IF(COALESCE(config_invoice_paid_notification_email, '') = '', 'billing@example.com', config_invoice_paid_notification_email), config_ticket_client_general_notifications = 1 WHERE company_id = 1"); $settings = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT config_ticket_prefix, config_ticket_next_number, config_invoice_prefix, config_invoice_next_number, config_quote_prefix, config_quote_next_number, config_sla_notification_email FROM settings WHERE company_id = 1")); $currency = $e((string) (mysqli_fetch_row(mysqli_query($mysqli, "SELECT company_currency FROM companies WHERE company_id = 1"))[0] ?? '') ?: 'USD'); $sla_id = intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT sla_id FROM slas WHERE sla_archived_at IS NULL ORDER BY sla_id LIMIT 1"))[0] ?? 0); $role_id = intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT role_id FROM user_roles ORDER BY role_id LIMIT 1"))[0] ?? 0); $account_id = intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT account_id FROM accounts ORDER BY account_id LIMIT 1"))[0] ?? 0); $hash = $e(password_hash(randomString(), PASSWORD_DEFAULT)); $client_id = $insert("INSERT INTO clients SET client_name = '" . $e($people['client']) . "', client_currency_code = '$currency'" . billingClientLegacySql()); $location_id = $insert("INSERT INTO locations SET location_name = 'Main office', location_address = '120 River Road', location_city = 'Springfield', location_state = 'PA', location_zip = '15001', location_primary = 1, location_client_id = $client_id"); [$alex_name, $alex_email, $alex_title] = $people['contact']; [$jamie_name, $jamie_email, $jamie_title] = $people['colleague']; $alex_user = $insert("INSERT INTO users SET user_name = '" . $e($alex_name) . "', user_email = '" . $e($alex_email) . "', user_password = '$hash', user_auth_method = 'local', user_type = 2, user_status = 1"); $jamie_user = $insert("INSERT INTO users SET user_name = '" . $e($jamie_name) . "', user_email = '" . $e($jamie_email) . "', user_password = '$hash', user_auth_method = 'local', user_type = 2, user_status = 1"); $alex = $insert("INSERT INTO contacts SET contact_name = '" . $e($alex_name) . "', contact_title = '" . $e($alex_title) . "', contact_email = '" . $e($alex_email) . "', contact_primary = 1, contact_important = 1, contact_billing = 1, contact_location_id = $location_id, contact_user_id = $alex_user, contact_client_id = $client_id"); $jamie = $insert("INSERT INTO contacts SET contact_name = '" . $e($jamie_name) . "', contact_title = '" . $e($jamie_title) . "', contact_email = '" . $e($jamie_email) . "', contact_technical = 1, contact_company_ticket_emails = 'new', contact_location_id = $location_id, contact_user_id = $jamie_user, contact_client_id = $client_id"); [$agent_name, $agent_email] = $people['agent']; $agent = $insert("INSERT INTO users SET user_name = '" . $e($agent_name) . "', user_email = '" . $e($agent_email) . "', user_password = '$hash', user_auth_method = 'local', user_type = 1, user_status = 1, user_role_id = $role_id"); // Tickets - one with a reply from us and one from them, and another for a list $prefix = $e($settings['config_ticket_prefix'] ?? ''); $number = max(1, intval($settings['config_ticket_next_number'] ?? 1)); $ticket = $insert("INSERT INTO tickets SET ticket_prefix = '$prefix', ticket_number = $number, ticket_source = 'Portal', ticket_subject = 'Front desk printer is offline', ticket_details = '
The printer at the front desk has shown offline since this morning. We have restarted it, but it still will not print.
', ticket_priority = 'High', ticket_status = 2, ticket_sla_id = $sla_id, ticket_billable = 0, ticket_url_key = '" . $e(randomString(32)) . "', ticket_response_due_at = NOW() + INTERVAL 1 HOUR, ticket_resolution_due_at = NOW() + INTERVAL 1 DAY, ticket_created_by = $me, ticket_assigned_to = $agent, ticket_client_id = $client_id, ticket_contact_id = $alex, ticket_location_id = $location_id"); $ticket2 = $insert("INSERT INTO tickets SET ticket_prefix = '$prefix', ticket_number = " . ($number + 1) . ", ticket_source = 'Email', ticket_subject = 'Laptop for the new receptionist', ticket_details = 'Sara starts on Monday and will need a laptop and an email account.
', ticket_priority = 'Medium', ticket_status = 2, ticket_billable = 0, ticket_url_key = '" . $e(randomString(32)) . "', ticket_created_by = $me, ticket_assigned_to = $agent, ticket_client_id = $client_id, ticket_contact_id = $alex"); $reply = 'Hi Alex,
I connected remotely and the print spooler had stopped. It is printing again now - could you send a test page to check?
Thanks,
' . escapeHtml(stripslashes((string) ($GLOBALS['session_name'] ?? ''))) . '
The test page came out fine - thank you!
'; $insert("INSERT INTO ticket_replies SET ticket_reply = '" . $e($client_reply) . "', ticket_reply_type = 'Client', ticket_reply_time_worked = '00:00:00', ticket_reply_by = $alex, ticket_reply_ticket_id = $ticket"); $insert("INSERT INTO ticket_watchers SET watcher_email = '" . $e($jamie_email) . "', watcher_ticket_id = $ticket"); $recurring = $insert("INSERT INTO recurring_tickets SET recurring_ticket_subject = 'Monthly backup check', recurring_ticket_details = 'Check last month''s backups ran, and test a restore.
', recurring_ticket_priority = 'Low', recurring_ticket_frequency = 'Monthly', recurring_ticket_billable = 0, recurring_ticket_start_date = CURDATE(), recurring_ticket_next_run = CURDATE(), recurring_ticket_created_by = $me, recurring_ticket_assigned_to = $agent, recurring_ticket_client_id = $client_id, recurring_ticket_contact_id = $alex"); $task = $insert("INSERT INTO tasks SET task_name = 'Replace the printer''s fuser unit', task_ticket_id = $ticket"); $approval = $insert("INSERT INTO task_approvals SET approval_scope = 'client', approval_type = 'any', approval_status = 'pending', approval_created_by = $me, approval_url_key = '" . $e(randomString(32)) . "', approval_task_id = $task"); // An invoice still owed, one paid, and a quote $invoice_prefix = $e($settings['config_invoice_prefix'] ?? ''); $invoice_number = max(1, intval($settings['config_invoice_next_number'] ?? 1)); $scope = $e('Managed IT services - ' . date('F Y')); $invoice = $insert("INSERT INTO invoices SET invoice_prefix = '$invoice_prefix', invoice_number = $invoice_number, invoice_scope = '$scope', invoice_status = 'Sent', invoice_date = CURDATE() - INTERVAL 10 DAY, invoice_due = CURDATE() + INTERVAL 20 DAY, invoice_amount = 1540.00, invoice_currency_code = '$currency', invoice_url_key = '" . $e(randomString(32)) . "', invoice_category_id = 0, invoice_client_id = $client_id"); $insert("INSERT INTO invoice_items SET item_name = 'Managed IT services', item_quantity = 1, item_price = 1250, item_subtotal = 1250, item_tax = 0, item_total = 1250, item_order = 1, item_invoice_id = $invoice"); $insert("INSERT INTO invoice_items SET item_name = 'Microsoft 365 Business Standard', item_quantity = 10, item_price = 29, item_subtotal = 290, item_tax = 0, item_total = 290, item_order = 2, item_invoice_id = $invoice"); $paid = $insert("INSERT INTO invoices SET invoice_prefix = '$invoice_prefix', invoice_number = " . ($invoice_number + 1) . ", invoice_scope = 'Printer repair', invoice_status = 'Paid', invoice_date = CURDATE() - INTERVAL 3 DAY, invoice_due = CURDATE() + INTERVAL 27 DAY, invoice_amount = 290.00, invoice_currency_code = '$currency', invoice_url_key = '" . $e(randomString(32)) . "', invoice_category_id = 0, invoice_client_id = $client_id"); $insert("INSERT INTO invoice_items SET item_name = 'Printer repair', item_quantity = 2, item_price = 145, item_subtotal = 290, item_tax = 0, item_total = 290, item_order = 1, item_invoice_id = $paid"); $insert("INSERT INTO payments SET payment_date = CURDATE(), payment_amount = 290.00, payment_currency_code = '$currency', payment_method = 'Credit Card', payment_reference = 'ch_3PxR1v2Sample', payment_account_id = $account_id, payment_invoice_id = $paid"); $quote_number = max(1, intval($settings['config_quote_next_number'] ?? 1)); $quote = $insert("INSERT INTO quotes SET quote_prefix = '" . $e($settings['config_quote_prefix'] ?? '') . "', quote_number = $quote_number, quote_scope = 'Network refresh', quote_status = 'Sent', quote_date = CURDATE(), quote_expire = CURDATE() + INTERVAL 30 DAY, quote_amount = 4280.00, quote_currency_code = '$currency', quote_url_key = '" . $e(randomString(32)) . "', quote_category_id = 0, quote_client_id = $client_id"); foreach ([['Firewall', 1, 1890], ['24-port switch', 2, 640], ['Installation and setup', 1, 1110]] as $order => [$name, $quantity, $price]) { $insert("INSERT INTO quote_items SET item_name = '" . $e($name) . "', item_quantity = $quantity, item_price = $price, item_subtotal = " . ($quantity * $price) . ", item_tax = 0, item_total = " . ($quantity * $price) . ", item_order = " . ($order + 1) . ", item_quote_id = $quote"); } // Shared links - a login sent, and one asked for $share = $insert("INSERT INTO shared_items SET item_active = 1, item_key = '" . $e(hash('sha256', randomString(32))) . "', item_type = 'Credential', item_related_id = 0, item_name = 'Microsoft 365 admin', item_note = 'Here is the admin login we talked about on the phone.', item_recipient = '" . $e($alex_email) . "', item_notify = 1, item_view_limit = 1, item_expire_at = NOW() + INTERVAL 1 DAY, item_created_by = $me, item_client_id = $client_id"); $request = $insert("INSERT INTO shared_items SET item_active = 1, item_key = '" . $e(hash('sha256', randomString(32))) . "', item_type = 'Request', item_related_id = 0, item_name = 'Wi-Fi controller login', item_recipient = '" . $e($alex_email) . "', item_notify = 1, item_view_limit = 1, item_expire_at = NOW() + INTERVAL 3 DAY, item_created_by = $me, item_client_id = $client_id"); // A calendar event with the contact invited $calendar = $insert("INSERT INTO calendars SET calendar_name = 'Client visits', calendar_color = '#0d6efd'"); $start = date('Y-m-d', strtotime('next tuesday')) . ' 10:00:00'; $event = $insert("INSERT INTO calendar_events SET event_title = 'Quarterly IT review', event_description = 'We will go over last quarter''s tickets and plan the next one.', event_start = '$start', event_end = '" . date('Y-m-d H:i:s', strtotime($start) + 3600) . "', event_all_day = 0, event_sequence = 0, event_created_by = $me, event_client_id = $client_id, event_location_id = $location_id, event_calendar_id = $calendar"); $insert("INSERT INTO calendar_event_attendees SET attendee_contact_id = $alex, attendee_event_id = $event"); return [ 'client_id' => $client_id, 'client' => $people['client'], 'location_id' => $location_id, 'alex' => $alex, 'alex_user' => $alex_user, 'alex_name' => $alex_name, 'alex_email' => $alex_email, 'jamie' => $jamie, 'jamie_user' => $jamie_user, 'jamie_name' => $jamie_name, 'jamie_email' => $jamie_email, 'agent' => $agent, 'agent_name' => $agent_name, 'agent_email' => $agent_email, 'ticket' => $ticket, 'ticket2' => $ticket2, 'reply' => $reply, 'client_reply' => $client_reply, 'recurring' => $recurring, 'approval' => $approval, 'invoice' => $invoice, 'paid' => $paid, 'quote' => $quote, 'share' => $share, 'request' => $request, 'event' => $event, 'sla_email' => (string) ($settings['config_sla_notification_email'] ?? ''), 'by' => stripslashes((string) ($GLOBALS['session_name'] ?? '')), ]; } /** Send an invoice email the way Send does (agent/post/invoice.php), without the PDF - $opts as billingInvoiceEmail() takes them. */ function emailPreviewInvoice($invoice_id, array $opts = []) { $invoice = billingRow('invoice', $invoice_id, false); [$subject, $email] = billingInvoiceEmail($invoice, $opts); billingEmailQueue(billingEmailRecipients($invoice['invoice_client_id']), $subject, $email); } /* * Every email, by where it goes: key => group, label, to (who gets it), when * (what sends it), run (sends it, given the sample) and pick (which address's * copy to show, when it goes to more than one). */ function emailPreviewCatalog() { $ticket_schedule = function ($s) { ticketSetSchedule(ticketRow($s['ticket']), date('Y-m-d', strtotime('+2 days')) . ' 10:30:00', 1); }; $reference = function ($s) { return ticketReference(ticketRow($s['ticket'])); }; return [ // ---- Tickets, to clients 'ticket_created' => ['group' => 'Tickets - to clients', 'label' => 'Ticket opened for them', 'to' => 'The contact and the watchers', 'when' => 'An agent opens a ticket for a contact', 'run' => function ($s) { ticketMailCreated($s['ticket'], 'Ticket Created -'); }], 'ticket_created_email' => ['group' => 'Tickets - to clients', 'label' => 'Their email became a ticket', 'to' => 'Whoever emailed', 'when' => 'The email parser makes a ticket from an email', 'run' => function ($s) { ticketMailCreated($s['ticket'], 'Ticket created -', [], false, 'email', [['email' => $s['alex_email'], 'name' => $s['alex_name']]]); }], 'ticket_created_scheduled' => ['group' => 'Tickets - to clients', 'label' => 'Scheduled ticket raised', 'to' => 'The contact and the watchers', 'when' => 'A recurring ticket raises its ticket', 'pick' => 'contact', 'run' => function ($s) { recurringTicketRaise($s['recurring'], 'Preview'); }], 'ticket_reply' => ['group' => 'Tickets - to clients', 'label' => 'Reply', 'to' => 'The contact and the watchers', 'when' => 'An agent replies publicly', 'run' => function ($s) { ticketMailReply($s['ticket'], $s['reply'], $s['by']); }], 'ticket_resolved' => ['group' => 'Tickets - to clients', 'label' => 'Resolved', 'to' => 'The contact and the watchers', 'when' => 'An agent resolves a ticket', 'run' => function ($s) { global $mysqli; mysqli_query($mysqli, "UPDATE tickets SET ticket_status = 4, ticket_resolved_at = NOW() WHERE ticket_id = " . intval($s['ticket'])); ticketMailReply($s['ticket'], $s['reply'], $s['by'], true); }], 'ticket_closed' => ['group' => 'Tickets - to clients', 'label' => 'Closed', 'to' => 'The contact and the watchers', 'when' => 'A ticket is closed, by an agent or after it was resolved', 'run' => function ($s) { global $mysqli; mysqli_query($mysqli, "UPDATE tickets SET ticket_status = 5, ticket_resolved_at = NOW(), ticket_closed_at = NOW() WHERE ticket_id = " . intval($s['ticket'])); ticketMailClosed($s['ticket']); }], 'ticket_watcher' => ['group' => 'Tickets - to clients', 'label' => 'Made a watcher', 'to' => 'The new watcher', 'when' => 'An agent adds a watcher and asks to tell them', 'run' => function ($s) { ticketMailWatchersAdded($s['ticket'], [$s['jamie_email']], $s['by']); }], 'ticket_colleague_new' => ['group' => 'Tickets - to clients', 'label' => 'New ticket at their company', 'to' => 'Primary and technical contacts who asked', 'when' => 'Anyone raises a ticket for their company', 'run' => function ($s) { ticketMailColleaguesNew($s['ticket']); }], 'ticket_approval' => ['group' => 'Tickets - to clients', 'label' => 'Approval needed', 'to' => 'The contact, or the technical or billing contacts', 'when' => 'An agent asks for a task to be approved', 'run' => function ($s) { ticketMailTaskApproval($s['approval'], $s['by']); }], 'ticket_scheduled' => ['group' => 'Tickets - to clients', 'label' => 'Work booked', 'to' => 'The contact and the watchers, with a calendar file', 'when' => 'An agent schedules the work', 'pick' => 'contact', 'run' => $ticket_schedule], 'ticket_schedule_cancelled' => ['group' => 'Tickets - to clients', 'label' => 'Booked work cancelled', 'to' => 'The contact and the watchers, with a calendar file', 'when' => 'An agent cancels the scheduled work', 'pick' => 'contact', 'run' => function ($s) use ($ticket_schedule) { $ticket_schedule($s); emailCaptureStart(); // only the cancellation ticketCancelSchedule(ticketRow($s['ticket'])); }], 'ticket_closed_reply' => ['group' => 'Tickets - to clients', 'label' => 'Replied to a closed ticket', 'to' => 'Whoever replied', 'when' => 'The email parser gets a reply to a closed ticket', 'run' => function ($s) { ticketMailClosedReply($s['ticket'], $s['alex_email']); }], // ---- Tickets, to the team 'agent_assigned' => ['group' => 'Tickets - to your team', 'label' => 'Assigned to you', 'to' => 'The agent', 'when' => 'Someone else assigns them a ticket', 'run' => function ($s) { ticketMailAssigned($s['agent'], [$s['ticket']], $s['by']); }], 'agent_assigned_many' => ['group' => 'Tickets - to your team', 'label' => 'Several assigned to you', 'to' => 'The agent', 'when' => 'Someone assigns them several at once, with Bulk Action', 'run' => function ($s) { ticketMailAssigned($s['agent'], [$s['ticket'], $s['ticket2']], $s['by']); }], 'agent_mentioned' => ['group' => 'Tickets - to your team', 'label' => 'Mentioned in a note', 'to' => 'The agent mentioned', 'when' => 'Someone @mentions them in an internal note', 'run' => function ($s) { $name = (string) getFieldById('users', $s['agent'], 'user_name'); ticketMailMentioned($s['ticket'], 0, '' . ticketMentionSpan($s['agent'], $name) . ' can you look at the firewall logs from last night before we call them back? The VPN dropped at 2:14 AM for everyone at the main office.
', $s['agent'], $s['by']); }], 'agent_recurring_assigned' => ['group' => 'Tickets - to your team', 'label' => 'Recurring ticket assigned', 'to' => 'The agent', 'when' => 'Someone else assigns them a recurring ticket', 'run' => function ($s) { recurringTicketMailAssigned($s['agent'], ['Monthly backup check'], $s['by']); }], 'agent_new_ticket' => ['group' => 'Tickets - to your team', 'label' => 'New ticket', 'to' => 'The new-ticket address (Admin > Ticket)', 'when' => 'A ticket is raised in the portal, by email or on a schedule', 'run' => function ($s) { ticketMailNewTicketAddress($s['ticket'], 'portal'); }], 'agent_client_replied' => ['group' => 'Tickets - to your team', 'label' => 'Client replied', 'to' => 'The agent it is assigned to', 'when' => 'The client replies in the portal or by email', 'run' => function ($s) { ticketMailAssigneeReplied($s['ticket'], 'portal', $s['client_reply'], $s['alex_name']); }], 'agent_sla_risk' => ['group' => 'Tickets - to your team', 'label' => 'SLA at risk', 'to' => 'The SLA alert address and the assigned agent', 'when' => 'A ticket nears its response or resolution target', 'pick' => 'agent', 'run' => function ($s) { ticketSlaAlert($s['ticket'], 'response', false, date('Y-m-d H:i:s', time() + 1800), $s['sla_email']); }], 'agent_sla_missed' => ['group' => 'Tickets - to your team', 'label' => 'SLA missed', 'to' => 'The SLA alert address and the assigned agent', 'when' => 'A ticket misses its response or resolution target', 'pick' => 'agent', 'run' => function ($s) { ticketSlaAlert($s['ticket'], 'resolution', true, date('Y-m-d H:i:s', time() - 3600), $s['sla_email']); }], 'agent_scheduled' => ['group' => 'Tickets - to your team', 'label' => 'Work booked', 'to' => 'The agent it is assigned to, with a calendar file', 'when' => 'Someone schedules the work', 'pick' => 'agent', 'run' => $ticket_schedule], 'agent_approval' => ['group' => 'Tickets - to your team', 'label' => 'Approval needed', 'to' => 'The agent asked', 'when' => 'Someone asks an agent to approve a task', 'run' => function ($s) { global $mysqli; mysqli_query($mysqli, "UPDATE task_approvals SET approval_scope = 'internal', approval_type = 'specific', approval_required_user_id = " . intval($s['agent']) . " WHERE approval_id = " . intval($s['approval'])); ticketMailTaskApproval($s['approval'], $s['by']); }], // ---- Billing 'invoice' => ['group' => 'Billing', 'label' => 'Invoice', 'to' => 'The primary and billing contacts, with the PDF', 'when' => 'An invoice is sent, by hand or as it recurs', 'run' => function ($s) { emailPreviewInvoice($s['invoice']); }], 'invoice_reminder' => ['group' => 'Billing', 'label' => 'Reminder - due soon', 'to' => 'The primary and billing contacts, with the PDF', 'when' => 'Send Reminder, or the nightly reminders (Admin > Invoice)', 'run' => function ($s) { global $mysqli; mysqli_query($mysqli, "UPDATE invoices SET invoice_due = CURDATE() + INTERVAL 3 DAY WHERE invoice_id = " . intval($s['invoice'])); emailPreviewInvoice($s['invoice'], ['kind' => 'reminder']); }], 'invoice_due_today' => ['group' => 'Billing', 'label' => 'Reminder - due today', 'to' => 'The primary and billing contacts, with the PDF', 'when' => 'Send Reminder, or the nightly reminders', 'run' => function ($s) { global $mysqli; mysqli_query($mysqli, "UPDATE invoices SET invoice_due = CURDATE() WHERE invoice_id = " . intval($s['invoice'])); emailPreviewInvoice($s['invoice'], ['kind' => 'reminder']); }], 'invoice_overdue' => ['group' => 'Billing', 'label' => 'Reminder - overdue', 'to' => 'The primary and billing contacts, with the PDF', 'when' => 'Send Reminder, or the nightly reminders', 'run' => function ($s) { global $mysqli; mysqli_query($mysqli, "UPDATE invoices SET invoice_status = 'Overdue', invoice_due = CURDATE() - INTERVAL 11 DAY WHERE invoice_id = " . intval($s['invoice'])); emailPreviewInvoice($s['invoice'], ['kind' => 'reminder']); }], 'invoice_paid' => ['group' => 'Billing', 'label' => 'Paid invoice', 'to' => 'The primary and billing contacts, with the PDF', 'when' => 'A paid invoice is sent', 'run' => function ($s) { emailPreviewInvoice($s['paid']); }], 'payment_receipt' => ['group' => 'Billing', 'label' => 'Payment receipt', 'to' => 'The primary and billing contacts', 'when' => 'A payment is taken, with Email Receipt ticked', 'run' => function ($s) { $invoice = billingRow('invoice', $s['paid'], false); [$subject, $email] = billingPaymentEmail([['invoice' => $invoice, 'amount' => 290]], ['date' => date('Y-m-d'), 'method' => 'Credit Card', 'reference' => 'ch_3PxR1v2Sample', 'currency' => $invoice['invoice_currency_code']]); billingEmailQueue(billingEmailRecipients($invoice['invoice_client_id']), $subject, $email); }], 'quote' => ['group' => 'Billing', 'label' => 'Quote', 'to' => 'The primary contact, with the PDF', 'when' => 'A quote is sent', 'run' => function ($s) { $quote = billingRow('quote', $s['quote'], false); [$subject, $email] = billingQuoteEmail($quote); billingEmailQueue(billingEmailRecipients($quote['quote_client_id'], null, 'quote'), $subject, $email, ['from' => 'quote']); }], 'statement' => ['group' => 'Billing', 'label' => 'Statement', 'to' => 'The primary and billing contacts, with the PDF', 'when' => 'Send Statement on a client', 'run' => function ($s) { $statement = billingStatement($s['client_id']); [$subject, $email] = billingStatementEmail($statement); billingEmailQueue(billingEmailRecipients($s['client_id']), $subject, $email); }], 'card_saved' => ['group' => 'Billing', 'label' => 'Card saved', 'to' => 'The contact who saved it', 'when' => 'A contact saves a card in the client portal', 'run' => function ($s) { billingMailCardSaved($s['alex_email'], $s['alex_name'], $s['client'], 'visa', '4242', 8, intval(date('Y')) + 3); }], 'quote_accepted' => ['group' => 'Billing', 'label' => 'Quote accepted', 'to' => 'The quote notification address (Admin > Quote)', 'when' => 'A client accepts a quote online', 'run' => function ($s) { billingQuoteAnsweredNotify($s['quote'], true, $s['alex_name'], 'Looks good - please go ahead.'); }], 'quote_declined' => ['group' => 'Billing', 'label' => 'Quote declined', 'to' => 'The quote notification address (Admin > Quote)', 'when' => 'A client declines a quote online', 'run' => function ($s) { billingQuoteAnsweredNotify($s['quote'], false, $s['alex_name'], 'We are going to hold off until next year.'); }], 'payment_notify' => ['group' => 'Billing', 'label' => 'Invoice paid online', 'to' => 'The paid notification address (Admin > Invoice)', 'when' => 'A client pays online, or autopay charges a card', 'run' => function ($s) { billingPaymentNotify(billingRow('invoice', $s['paid'], false), 290, 'Online card'); }], ] + emailPreviewContractCatalog() + [ // ---- Client portal 'portal_invite' => ['group' => 'Client portal', 'label' => 'Invite', 'to' => 'The contact', 'when' => 'An agent gives a contact portal access and ticks Email them an invite', 'run' => function ($s) { portalMailInvite($s['alex_user']); }], 'portal_invite_colleague' => ['group' => 'Client portal', 'label' => 'Invite from a colleague', 'to' => 'The contact', 'when' => 'A primary contact gives someone access in the portal', 'run' => function ($s) { portalMailInvite($s['jamie_user'], $s['alex_name']); }], ] + emailPreviewInviteCatalog() + [ 'portal_reset' => ['group' => 'Client portal', 'label' => 'Forgot password', 'to' => 'The contact', 'when' => 'They ask for a link on the sign-in page', 'run' => function ($s) { portalMailPasswordReset($s['alex_user']); }], 'portal_password' => ['group' => 'Client portal', 'label' => 'Password changed', 'to' => 'The contact', 'when' => 'They change their password, or set it from a link', 'run' => function ($s) { portalMailAccountChange($s['alex_email'], $s['alex_name'], 'password'); }], 'portal_mfa_on' => ['group' => 'Client portal', 'label' => 'Two-step sign-in on', 'to' => 'The contact', 'when' => 'They turn on two-step sign-in', 'run' => function ($s) { portalMailAccountChange($s['alex_email'], $s['alex_name'], 'mfa_on'); }], 'portal_mfa_off' => ['group' => 'Client portal', 'label' => 'Two-step sign-in off', 'to' => 'The contact', 'when' => 'They turn it off, or an agent does', 'run' => function ($s) { portalMailAccountChange($s['alex_email'], $s['alex_name'], 'mfa_off_by_us'); }], // ---- Shared links 'share_link' => ['group' => 'Shared links', 'label' => 'Secure link', 'to' => 'Whoever it is for', 'when' => 'An agent shares something and asks to email the link', 'run' => function ($s) { global $config_base_url; shareEmailLink(shareRow($s['share']), "https://$config_base_url/guest/share.php?id=" . intval($s['share']) . '#sample-key', 'passphrase'); }], 'share_request' => ['group' => 'Shared links', 'label' => 'Login asked for', 'to' => 'Whoever it is asked of', 'when' => 'An agent asks a client for a login with a link', 'run' => function ($s) { global $config_base_url; shareEmailLink(shareRow($s['request']), "https://$config_base_url/guest/share.php?id=" . intval($s['request']) . '#sample-key', 'none'); }], 'share_code' => ['group' => 'Shared links', 'label' => 'Code to open a link', 'to' => 'Whoever it is for', 'when' => 'They open a link that is protected by an emailed code', 'run' => function ($s) { shareEmailCode(shareRow($s['share']), '482913'); }], 'share_sent_back' => ['group' => 'Shared links', 'label' => 'Login sent back', 'to' => 'The agent who asked for it', 'when' => 'A client sends a login back through a link', 'run' => function ($s) { $row = shareRow($s['request']); shareNotifyCreator($row, 'Login sent back', 'A login was sent back for ' . shareName($row) . ' - ' . $row['client_name'], true); }], // ---- Calendar 'event_invite' => ['group' => 'Calendar', 'label' => 'Invitation', 'to' => 'The people invited, with a calendar file', 'when' => 'An event is saved with Send invitations ticked', 'run' => function ($s) { calendarSendInvites(calendarEventRow($s['event']), 'invite'); }], 'event_update' => ['group' => 'Calendar', 'label' => 'Event changed', 'to' => 'The people invited, with a calendar file', 'when' => 'An event they were invited to changes', 'run' => function ($s) { calendarSendInvites(calendarEventRow($s['event']), 'update'); }], 'event_cancel' => ['group' => 'Calendar', 'label' => 'Event cancelled', 'to' => 'The people invited, with a calendar file', 'when' => 'An event is deleted, or someone is taken off it', 'run' => function ($s) { calendarSendInvites(calendarEventRow($s['event']), 'cancel'); }], ] + emailPreviewUserInviteCatalog() + [ // ---- Your team's accounts 'user_changed' => ['group' => 'Your team\'s accounts', 'label' => 'Account changed', 'to' => 'The agent', 'when' => 'Their password, email or MFA changes', 'run' => function ($s) { userNotifyChange($s['agent_name'], $s['agent_email'], 'your password was changed'); }], 'user_new_device' => ['group' => 'Your team\'s accounts', 'label' => 'Sign in from a new device', 'to' => 'The agent', 'when' => 'They sign in from a new address and browser', 'run' => function ($s) { userMailSignInAlert($s['agent_name'], $s['agent_email'], 'new', '203.0.113.24', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0 Safari/537.36'); }], 'user_mfa_failed' => ['group' => 'Your team\'s accounts', 'label' => 'Wrong MFA code', 'to' => 'The agent', 'when' => 'Someone uses their password with a wrong MFA code', 'run' => function ($s) { userMailSignInAlert($s['agent_name'], $s['agent_email'], 'mfa', '198.51.100.7', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) Gecko/20100101 Firefox/129.0'); }], 'admin_sign_in_alert' => ['group' => 'Your team\'s accounts', 'label' => 'Failed sign-ins', 'to' => 'Every admin', 'when' => 'A run of failed sign-ins from one IP address or against one account (Admin > Settings > Security)', 'run' => function ($s) { signInMailAlert($s['agent_name'], $s['agent_email'], '10 failed sign-ins from 198.51.100.7', '10 failed sign-ins in 10 minutes from 198.51.100.7, trying ' . $s['agent_name'] . ' and 3 emails with no account.', ['IP address' => '198.51.100.7', 'Accounts tried' => 'Trying ' . $s['agent_name'] . ' and 3 emails with no account'], '/admin/sign_ins.php?view=ips&q=198.51.100.7'); }], // ---- Other 'bulk' => ['group' => 'Other', 'label' => 'Email to many contacts', 'to' => 'The contacts picked', 'when' => 'Email on the contacts or clients list', 'run' => function ($s) { emailBulkSend([['email' => $s['alex_email'], 'name' => $s['alex_name'], 'footnote' => 'You received this as a contact for ' . $s['client'] . ' with ' . (billingCompany()['company_name'] ?? '') . '.']], array_key_first(emailMailboxes()) ?? 'system', '', 'Our office is closed on Friday', 'Our office is closed this Friday for staff training. Support is still there for anything urgent - call us as usual.
Have a good weekend!
', true); }], 'test' => ['group' => 'Other', 'label' => 'Test email', 'to' => 'Whoever it is sent to', 'when' => 'Send a test in Admin > Mail', 'run' => function ($s) { emailTestSend('you@example.com', 'system'); }], ]; } /* * A sample contract for the client - Managed IT Services, its contact Alex * and the sample agent looking after it. $ending: active, a year in and * coming up to its notice date; else a draft starting next week. Its row. */ function emailPreviewContract(array $s, $ending = false) { global $mysqli; $dates = $ending ? "contract_status = 'Active', contract_start_date = CURDATE() - INTERVAL 11 MONTH + INTERVAL 14 DAY, contract_end_date = CURDATE() + INTERVAL 1 MONTH + INTERVAL 13 DAY" : "contract_status = 'Draft', contract_start_date = CURDATE() + INTERVAL 7 DAY, contract_end_date = CURDATE() + INTERVAL 1 YEAR + INTERVAL 6 DAY"; mysqli_query($mysqli, "INSERT INTO contracts SET contract_name = 'Managed IT Services', contract_type = 'managed', $dates, contract_term_months = 12, contract_auto_renew = 1, contract_notice_days = 30, contract_rate_standard = 150.00, contract_net_terms = 30, contract_support_hours = 'Mon-Fri 8am-6pm', contract_details = 'We look after every computer, server and network device at [Client], and answer tickets within the response times above.
', contract_client_id = " . intval($s['client_id']) . ", contract_contact_id = " . intval($s['alex']) . ", contract_owner_id = " . intval($s['agent'])); return contractRow(intval(mysqli_insert_id($mysqli))); } /* The contract emails for the catalog - once the contracts updates have run. */ function emailPreviewContractCatalog() { if (!contractsReady()) { return []; } $catalog = [ // ---- Contracts 'contract_reminder' => ['group' => 'Contracts', 'label' => 'Coming up to its notice date', 'to' => 'Whoever looks after the contract', 'when' => 'The nightly run, a month before a contract\'s notice date', 'run' => function ($s) { contractRemind(emailPreviewContract($s, true)); }], ]; if (!contractSignaturesReady()) { return $catalog; } return $catalog + [ 'contract_sign_request' => ['group' => 'Contracts', 'label' => 'Please sign', 'to' => 'Who it is sent to', 'when' => 'An agent sends a contract to sign', 'run' => function ($s) { contractSignatureSend(emailPreviewContract($s), $s['alex_name'], $s['alex_email'], $s['alex'], 'Here is the agreement we went over on Tuesday - let me know if anything needs changing.', $s['agent']); }], 'contract_client_signed' => ['group' => 'Contracts', 'label' => 'Signed by the client', 'to' => 'Whoever looks after the contract', 'when' => 'The client signs it, from the email or in the portal', 'run' => function ($s) { $row = emailPreviewContract($s); [$round_id] = contractSignatureSend($row, $s['alex_name'], $s['alex_email'], $s['alex'], '', $s['agent']); emailCaptureStop(); emailCaptureStart(); contractSignatureClientSign(contractSignatureRow($round_id), ['name' => $s['alex_name'], 'title' => 'Practice Manager', 'image' => null], 'portal', $s['alex']); }], 'contract_signed_copy' => ['group' => 'Contracts', 'label' => 'The signed copy', 'to' => 'Who signed it, and who it was sent to', 'when' => 'An agent countersigns - the PDF signed by both goes with it', 'run' => function ($s) { global $mysqli; $row = emailPreviewContract($s); [$round_id] = contractSignatureSend($row, $s['alex_name'], $s['alex_email'], $s['alex'], '', $s['agent']); emailCaptureStop(); emailCaptureStart(); mysqli_query($mysqli, "UPDATE contract_signatures SET contract_signature_client_name = '" . escapeSql($s['alex_name']) . "', contract_signature_client_signed_at = NOW(), contract_signature_client_via = 'link', contract_signature_agent_name = '" . escapeSql($s['agent_name']) . "', contract_signature_agent_signed_at = NOW() WHERE contract_signature_id = " . intval($round_id)); contractSignatureSignedEmail(contractSignatureRow($round_id), false); }], ] + (contractHoursReady() ? [ 'contract_hours_low' => ['group' => 'Contracts', 'label' => 'Prepaid hours running low', 'to' => 'Whoever looks after the contract', 'when' => 'Tickets drawn from a contract\'s prepaid hours take them under the hours set to be told at', 'run' => function ($s) { $row = emailPreviewContract($s, true); $row['contract_name'] = 'Support Block - 20 hours'; $row['contract_hours_alert'] = 4; contractHoursAlertSend($row, ['bought' => 20.0, 'used' => 17.5, 'left' => 2.5, 'lapsed' => 0.0, 'next_expiry' => date('Y-m-d', strtotime('+40 days')), 'next_expiry_hours' => 2.5, 'blocks' => 1]); }], ] : []); } /* A sample invite or sign-up for Riley - by the agent, by Alex in the portal (by_contact), or a sign-up. Its id. */ function emailPreviewInvite(array $s, $kind = 'invite', $by_contact = false) { global $mysqli; [$name, $email] = EMAIL_PREVIEW_PEOPLE['newcomer']; mysqli_query($mysqli, "INSERT INTO portal_invites SET invite_kind = '" . ($kind === 'signup' ? 'signup' : 'invite') . "', invite_email = '" . escapeSql($email) . "', invite_name = " . ($kind === 'signup' ? 'NULL' : "'" . escapeSql($name) . "'") . ", invite_technical = " . ($kind === 'signup' ? 0 : 1) . ", invite_expires_at = NOW(), invite_created_by = " . ($kind === 'signup' || $by_contact ? 0 : intval($s['agent'])) . ", invite_created_by_contact_id = " . ($by_contact ? intval($s['alex']) : 0) . ", invite_client_id = " . intval($s['client_id'])); return intval(mysqli_insert_id($mysqli)); } /* The invite and sign-up emails for the catalog - once the 3.4.6 update has run. */ function emailPreviewInviteCatalog() { if (!portalInvitesReady()) { return []; } return [ 'portal_join_invite' => ['group' => 'Client portal', 'label' => 'Invite to join', 'to' => 'Who is invited', 'when' => 'An agent invites someone from the client\'s Contacts > Invites', 'run' => function ($s) { $invite_id = emailPreviewInvite($s); portalInviteMail($invite_id, portalInviteNewToken($invite_id)); }], 'portal_join_invite_colleague' => ['group' => 'Client portal', 'label' => 'Invite to join from a colleague', 'to' => 'Who is invited', 'when' => 'A primary contact invites someone from Contacts in the portal', 'run' => function ($s) { $invite_id = emailPreviewInvite($s, 'invite', true); portalInviteMail($invite_id, portalInviteNewToken($invite_id)); }], 'portal_join_accepted' => ['group' => 'Client portal', 'label' => 'Invite accepted', 'to' => 'The primary contact who sent it', 'when' => 'Someone they invited joins', 'run' => function ($s) { global $mysqli; $invite_id = emailPreviewInvite($s, 'invite', true); [$name, $email] = EMAIL_PREVIEW_PEOPLE['newcomer']; mysqli_query($mysqli, "INSERT INTO contacts SET contact_name = '" . escapeSql($name) . "', contact_email = '" . escapeSql($email) . "', contact_technical = 1, contact_client_id = " . intval($s['client_id'])); mysqli_query($mysqli, "UPDATE portal_invites SET invite_accepted_at = NOW(), invite_contact_id = " . intval(mysqli_insert_id($mysqli)) . " WHERE invite_id = $invite_id"); portalMailInviteAccepted($invite_id); }], 'portal_signup' => ['group' => 'Client portal', 'label' => 'Finish signing up', 'to' => 'Who is signing up', 'when' => 'Someone asks to sign up from the sign-in page, with an address at a client that lets people sign up', 'run' => function ($s) { $invite_id = emailPreviewInvite($s, 'signup'); portalInviteMail($invite_id, portalInviteNewToken($invite_id)); }], 'portal_signup_exists' => ['group' => 'Client portal', 'label' => 'Signing up with an account already', 'to' => 'Who tried to sign up', 'when' => 'Someone asks to sign up with an address that signs in already', 'run' => function ($s) { portalMailSignupExists($s['jamie_user']); }], 'portal_signup_joined' => ['group' => 'Client portal', 'label' => 'Someone signed up', 'to' => 'The client\'s primary contact', 'when' => 'Someone finishes signing up', 'run' => function ($s) { global $mysqli; [$name, $email] = EMAIL_PREVIEW_PEOPLE['newcomer']; mysqli_query($mysqli, "INSERT INTO contacts SET contact_name = '" . escapeSql($name) . "', contact_email = '" . escapeSql($email) . "', contact_client_id = " . intval($s['client_id'])); portalMailSignupJoined(intval(mysqli_insert_id($mysqli))); }], ]; } /* The agent invite for the catalog - once the 3.5.1 update has run. Riley, invited by you as a technician, MFA required. */ function emailPreviewUserInviteCatalog() { if (!userInvitesReady()) { return []; } return [ 'user_invite' => ['group' => 'Your team\'s accounts', 'label' => 'Invite to join', 'to' => 'Who is invited', 'when' => 'An admin invites someone from New User or Get started', 'run' => function ($s) { global $mysqli, $session_user_id; [$name, $email] = EMAIL_PREVIEW_PEOPLE['newcomer']; $role_id = intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT role_id FROM user_roles WHERE role_archived_at IS NULL AND role_is_admin = 0 ORDER BY role_name = 'Technician' DESC, role_id LIMIT 1"))[0] ?? 0); mysqli_query($mysqli, "INSERT INTO user_invites SET invite_name = '" . escapeSql($name) . "', invite_email = '" . escapeSql($email) . "', invite_role_id = $role_id, invite_force_mfa = 1, invite_expires_at = NOW() + INTERVAL " . USER_INVITE_LINK_DAYS . " DAY, invite_created_by = " . intval($session_user_id)); userInviteMail(intval(mysqli_insert_id($mysqli)), bin2hex(random_bytes(24))); }], ]; } /* * Send yourself a copy of a preview - to $to (raw), its subject marked * [Preview], from the mailbox it would come from, without its calendar file * (it would add the sample event to your calendar). True when it was queued. */ function emailPreviewSendCopy($key, $to, $name) { global $mysqli; $preview = emailPreview($key); $email = $preview['email'] ?? null; if (!$email || !emailReady() || !filter_var($to, FILTER_VALIDATE_EMAIL)) { return false; } $from = $email['from'] !== '' ? ['email' => $email['from'], 'name' => $email['from_name']] : emailFrom(); addToMailQueue([[ 'from' => mysqli_real_escape_string($mysqli, $from['email']), 'from_name' => mysqli_real_escape_string($mysqli, $from['name']), 'recipient' => mysqli_real_escape_string($mysqli, (string) $to), 'recipient_name' => mysqli_real_escape_string($mysqli, (string) $name), 'subject' => mysqli_real_escape_string($mysqli, '[Preview] ' . $email['subject']), 'body' => mysqli_real_escape_string($mysqli, $email['html']), ]]); return true; } /* * One email from the catalog, as it would go out - ['emails' => what was * captured (emailCaptureStop()), 'email' => the copy to show, 'error' => why * it failed]. null for no such email. Nothing it does is kept. */ function emailPreview($key) { global $mysqli, $config_ticket_client_general_notifications; $catalog = emailPreviewCatalog(); if (!isset($catalog[$key])) { return null; } $entry = $catalog[$key]; $notifications = $config_ticket_client_general_notifications ?? 0; $config_ticket_client_general_notifications = 1; $error = ''; $sample = []; mysqli_begin_transaction($mysqli); emailCaptureStart(); try { $sample = emailPreviewSample(); ($entry['run'])($sample); } catch (Throwable $e) { $error = $e->getMessage(); } finally { $emails = emailCaptureStop(); // Something that ended the transaction early would have kept the sample records - say so if (intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT @@in_transaction"))[0] ?? 0) !== 1) { logApp('Mail', 'error', 'The email preview ' . escapeSql($key) . ' ended its transaction early, so its sample records may have been kept'); } mysqli_rollback($mysqli); $config_ticket_client_general_notifications = $notifications; } $pick = ['contact' => $sample['alex_email'] ?? '', 'agent' => $sample['agent_email'] ?? ''][$entry['pick'] ?? ''] ?? ''; $show = null; foreach ($emails as $email) { if ($pick === '' || strcasecmp($email['to'], $pick) === 0) { $show = $email; break; } } return ['emails' => $emails, 'email' => $show ?? ($emails[0] ?? null), 'error' => $error] + $entry; }