'#0d6efd', 'success' => '#198754', 'warning' => '#b35c00', 'danger' => '#c42b37', 'secondary' => '#5c636a']; /* * The line at the top of an email people reply to. cron/ticket_email_parser.php * keeps only what is above it in a reply, so it goes first - before anything * a mail program quotes back. */ const EMAIL_REPLY_MARKER = '##- Please type your reply above this line -##'; /** * One email as HTML. $email, every value raw - it is escaped here, but for * message.html, which is HTML: * marker true for an email people reply to - the reply line goes first * preview the line an inbox shows beside the subject (hidden in the email) * kind the small heading - Invoice, Ticket update, Resolved * tone its colour - primary, success, warning, danger or secondary * title the heading * name who it is to - "Hello Name,"; '' greets nobody by name ("Hello,"), null not at all * intro paragraphs before the summary * content HTML someone wrote that is the email itself - a bulk email, say - made safe * (emailMessageHtml()) and shown as it is, not quoted * note the agent's own words, kept as typed * message ['html' => what someone wrote, as HTML, 'by' => who, 'when' => when, 'label' => the line over it] * summary label => value; strong names the label to make stand out * lines a table - ['head' => [...], 'align' => [...], 'rows' => [[cell, ...]]], a cell * plain text or ['text' => ..., 'url' => ..., 'tone' => danger] * callout a paragraph that stands out, in the tone's colour * button ['label' => ..., 'url' => ..., 'show_url' => true to print the address under it, * for a link that has to be copied whole] * after paragraphs after the button, smaller * contact the foot's contact line, in place of the company's phone, email and website * footnote a line under the card - why they got it */ function emailHtml(array $email) { global $config_base_url; $e = function ($value) { return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'); }; $tones = EMAIL_TONES; $accent = $tones[$email['tone'] ?? 'primary'] ?? $tones['primary']; $text = '#212529'; $muted = '#6c757d'; $line = '#e5e7eb'; $font = "font-family:-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;"; $company = billingCompany(); $company_name = (string) ($company['company_name'] ?? ''); $paragraph = function ($text_value) use ($e, $text) { return '
' . nl2br($e($text_value)) . '
'; }; $logo = !empty($company['logo_url']) && !empty($config_base_url) ? '| ' . $e(trim($email['note'])) . ' |
| '
. $head . ' ' . emailMessageHtml($email['message']['html']) . ' |
' . nl2br($e(trim($email['callout']))) . '
'; } if (!empty($email['button']['url'])) { $body .= '| ' . '' . $e($email['button']['label']) . '' . ' |
Or copy this into your browser: ' . $e($email['button']['url']) . '
'; } } foreach ((array) ($email['after'] ?? []) as $text_value) { $body .= '' . nl2br($e($text_value)) . '
'; } $contact = array_key_exists('contact', $email) ? (string) $email['contact'] : implode(' ยท ', array_filter([$company['phone_display'] ?? '', $company['company_email'] ?? '', $company['company_website'] ?? ''])); $foot = array_filter([ $company_name, formatAddress($company['company_address'] ?? '', $company['company_city'] ?? '', $company['company_state'] ?? '', $company['company_zip'] ?? '', $company['company_country'] ?? '', ', '), $contact, ]); // The reply line - first, so a reply quoting this email is cut off cleanly $marker = !empty($email['marker']) ? ''
. ($marker !== '' ? '
' . $e($email['footnote']) . ' ' : '')
. ' |
..., 'name' => ..., 'greet' => name to greet (or
* '' for "Hello,", null for none), 'footnote' => why they get it], ...].
* $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
* queued_at when to send it, 'Y-m-d H:i:s' - now when not given
* always queue it even without mail set up, to go once it is - billing's Send
* windows have always done that (billingEmailQueue())
* 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;
if (!emailReady() && empty($opts['always'])) {
return [];
}
$from = $opts['from'] ?? emailFrom();
$queued_at = !empty($opts['queued_at']) && strtotime((string) $opts['queued_at']) ? date('Y-m-d H:i:s', strtotime((string) $opts['queued_at'])) : '';
$attachments = array_values(array_filter((array) ($opts['attachments'] ?? [])));
$data = [];
$sent = [];
foreach ($recipients as $recipient) {
$address = trim((string) ($recipient['email'] ?? ''));
if (!filter_var($address, FILTER_VALIDATE_EMAIL) || isset($sent[strtolower($address)])) {
continue;
}
$name = (string) ($recipient['name'] ?? '');
$html = emailHtml(array_merge($email, [
'name' => array_key_exists('greet', $recipient) ? $recipient['greet'] : $name,
], isset($recipient['footnote']) ? ['footnote' => $recipient['footnote']] : []));
$sent[strtolower($address)] = $name !== '' ? "$name ($address)" : $address;
if (emailCapturing()) {
$GLOBALS['itflow_email_capture'][] = ['to' => $address, 'to_name' => $name, 'from' => (string) $from['email'], 'from_name' => (string) $from['name'],
'subject' => (string) $subject, 'html' => $html, 'attachments' => $attachments, 'cal_str' => (string) ($opts['cal_str'] ?? '')];
continue;
}
$data[] = [
'from' => mysqli_real_escape_string($mysqli, (string) $from['email']),
'from_name' => mysqli_real_escape_string($mysqli, (string) $from['name']),
'recipient' => mysqli_real_escape_string($mysqli, $address),
'recipient_name' => mysqli_real_escape_string($mysqli, $name !== '' ? $name : $address),
'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']] : [])
+ ($queued_at !== '' ? ['queued_at' => $queued_at] : []);
}
if ($data) {
addToMailQueue($data);
}
return array_values($sent);
}
/** The test email in Admin > Mail, from one of the mailboxes - system, ticket, invoice or quote. Who it went to - [] when it didn't. */
function emailTestSend($to, $mailbox) {
$box = ['ticket' => emailFrom('ticket'), 'invoice' => billingEmailFrom('invoice'), 'quote' => billingEmailFrom('quote')][$mailbox] ?? emailFrom();
$which = ['system' => 'the system mailbox', 'ticket' => 'the ticket mailbox', 'invoice' => 'the invoice mailbox', 'quote' => 'the quote mailbox'][$mailbox] ?? 'the system mailbox';
return emailQueue([['email' => trim((string) $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($box['name'] . ' <' . $box['email'] . '>'), 'To' => trim((string) $to), 'Sent' => date('D j M Y, g:i A')],
], ['from' => ['email' => $box['email'], 'name' => $box['name']]]);
}
/*
* Send an email from the contacts or clients list (agent/includes/bulk_email_fields.php)
* to $recipients (emailBulkSend()'s), $picked how many were asked for - then
* say what happened and go back. Refuses without mail set up, a mailbox, a
* subject or a message.
*/
function emailBulkPost(array $recipients, $picked) {
global $session_name;
$mailbox = (string) ($_POST['mailbox'] ?? '');
$subject = trim((string) ($_POST['subject'] ?? ''));
$body = (string) ($_POST['body'] ?? '');
$at = trim((string) ($_POST['queued_at'] ?? ''));
$queued_at = $at !== '' && strtotime($at) ? date('Y-m-d H:i:s', strtotime($at)) : '';
if (!emailReady()) {
flashAlert('Outgoing mail is not set up, so nothing was sent', 'error');
redirect();
}
if (!isset(emailMailboxes()[$mailbox])) {
flashAlert('Pick the mailbox it comes from', 'error');
redirect();
}
if ($subject === '' || trim(strip_tags($body, '')) === '') {
flashAlert('Give it a subject and a message - nothing was sent', 'error');
redirect();
}
$sent = emailBulkSend($recipients, $mailbox, $_POST['mail_from_name'] ?? '', $subject, $body, !empty($_POST['greet']), $queued_at);
$count = count($sent);
if (!$count) {
flashAlert('None of them has an email address, so nothing was sent', 'error');
redirect();
}
$later = $queued_at !== '' && strtotime($queued_at) > time() ? ' to go ' . date('M j \a\t g:i A', strtotime($queued_at)) : '';
logAudit("Bulk Mail", "Send", "$session_name sent " . escapeSql($subject) . " to $count " . ($count === 1 ? 'contact' : 'contacts') . " via bulk mail" . ($later !== '' ? escapeSql($later) : ''));
$left_out = max(0, intval($picked) - $count);
flashAlert("$count email" . ($count === 1 ? '' : 's') . " queued$later" . ($left_out ? " - $left_out left out with no email address, or the same one" : ''));
redirect();
}
/*
* An email an agent writes to many contacts - Email on the contacts or clients
* list - in the email look, with the logo and the company's details. Each
* $recipients entry: email, name and footnote (why they get it). $body is the
* agent's HTML; $greet starts it "Hello Name,"; $from_name '' keeps the
* mailbox's own; $queued_at when to send ('' for now). Who it was queued to.
*/
function emailBulkSend(array $recipients, $mailbox, $from_name, $subject, $body, $greet = false, $queued_at = '') {
$boxes = emailMailboxes();
// A preview shows it before any mailbox is set up
$box = $boxes[$mailbox] ?? (emailCapturing() ? emailFrom() : null);
if (!$box) {
return [];
}
$from_name = trim((string) $from_name);
if (!$greet) {
$recipients = array_map(function ($recipient) {
return ['greet' => null] + $recipient;
}, $recipients);
}
return emailQueue($recipients, trim((string) $subject), [
'title' => trim((string) $subject),
'preview' => mb_substr(trim(preg_replace('/\s+/', ' ', html_entity_decode(strip_tags((string) $body), ENT_QUOTES, 'UTF-8'))), 0, 140),
'content' => (string) $body,
], ['from' => ['email' => $box['email'], 'name' => $from_name !== '' ? $from_name : $box['name']], 'queued_at' => $queued_at]);
}