to avoid false positives. * Regex separators has "regex:" in the beginning. */ public static $alternative_reply_separators = [ self::REPLY_SEPARATOR_HTML, // Our HTML separator self::REPLY_SEPARATOR_TEXT, // Our plain text separator // Email service providers specific separators. '
/', // MS Outlook
// General separators.
'regex:/])*>/', // General sepator. Should skip Gmail's
.
'',
'‐‐‐‐‐‐‐ Original Message ‐‐‐‐‐‐‐',
'--------------- Original Message ---------------',
'-------- Αρχικό μήνυμα --------', // Greek
];
/**
* md5 of the last applied mail config.
*/
public static $last_mail_config_hash = '';
/**
* Used to get SMTP queue id when sending emails to customers.
*/
public static $smtp_queue_id_plugin_registered = false;
/**
* Configure mail sending parameters.
*
* @param App\Mailbox $mailbox
* @param App\User $user_from
* @param App\Conversation $conversation
*/
public static function setMailDriver($mailbox = null, $user_from = null, $conversation = null)
{
if ($mailbox) {
// Configure mail driver according to Mailbox settings
\Config::set('mail.driver', $mailbox->getMailDriverName());
\Config::set('mail.from', $mailbox->getMailFrom($user_from, $conversation));
// SMTP
if ($mailbox->out_method == Mailbox::OUT_METHOD_SMTP) {
\Config::set('mail.host', $mailbox->out_server);
\Config::set('mail.port', $mailbox->out_port);
if (!$mailbox->out_username) {
\Config::set('mail.username', null);
\Config::set('mail.password', null);
} else {
\Config::set('mail.username', $mailbox->out_username);
\Config::set('mail.password', $mailbox->out_password);
}
\Config::set('mail.encryption', $mailbox->getOutEncryptionName());
}
} else {
// Use default settings
\Config::set('mail.driver', \Config::get('mail.driver'));
\Config::set('mail.from', ['address' => self::getSystemMailFrom(), 'name' => '']);
}
self::reapplyMailConfig();
}
/**
* Reapply new mail config.
*/
public static function reapplyMailConfig()
{
// Check hash to avoid recreating MailServiceProvider.
$mail_config_hash = md5(json_encode(\Config::get('mail')));
if (self::$last_mail_config_hash != $mail_config_hash) {
self::$last_mail_config_hash = $mail_config_hash;
} else {
return false;
}
// Without doing this, Swift mailer uses old config values
// if there were emails sent with previous config.
\App::forgetInstance('mailer');
\App::forgetInstance('swift.mailer');
\App::forgetInstance('swift.transport');
(new \Illuminate\Mail\MailServiceProvider(app()))->register();
// We have to update Mailer facade manually, as it does not happen automatically
// and previous instance of app('mailer') is used.
\Mail::swap(app('mailer'));
\Eventy::action('mail.reapply_mail_config');
}
/**
* Set system mail driver for sending system emails to users.
*
* @param App\Mailbox $mailbox
*/
public static function setSystemMailDriver()
{
\Config::set('mail.driver', self::getSystemMailDriver());
\Config::set('mail.from', [
'address' => self::getSystemMailFrom(),
'name' => Option::get('company_name', \Config::get('app.name')),
]);
// SMTP
if (\Config::get('mail.driver') == self::MAIL_DRIVER_SMTP) {
\Config::set('mail.host', Option::get('mail_host'));
\Config::set('mail.port', Option::get('mail_port'));
if (!Option::get('mail_username')) {
\Config::set('mail.username', null);
\Config::set('mail.password', null);
} else {
\Config::set('mail.username', Option::get('mail_username'));
\Config::set('mail.password', \Helper::decrypt(Option::get('mail_password')));
}
\Config::set('mail.encryption', Option::get('mail_encryption'));
}
self::reapplyMailConfig();
}
/**
* Replace mail vars in the text.
*/
public static function replaceMailVars($text, $data = [], $escape = false, $remove_non_replaced = false)
{
// Available variables to insert into email in UI.
$vars = [];
if (!empty($data['conversation'])) {
$vars['{%subject%}'] = $data['conversation']->subject;
$vars['{%conversation.number%}'] = $data['conversation']->number;
$vars['{%customer.email%}'] = $data['conversation']->customer_email;
}
if (!empty($data['mailbox'])) {
$vars['{%mailbox.email%}'] = $data['mailbox']->email;
$vars['{%mailbox.name%}'] = $data['mailbox']->name;
// To avoid recursion.
if (isset($data['mailbox_from_name'])) {
$vars['{%mailbox.fromName%}'] = $data['mailbox_from_name'];
} else {
$vars['{%mailbox.fromName%}'] = $data['mailbox']->getMailFrom(!empty($data['user']) ? $data['user'] : null)['name'];
}
}
if (!empty($data['customer'])) {
$vars['{%customer.fullName%}'] = $data['customer']->getFullName(true);
$vars['{%customer.firstName%}'] = $data['customer']->getFirstName(true);
$vars['{%customer.lastName%}'] = $data['customer']->last_name;
$vars['{%customer.company%}'] = $data['customer']->company;
}
if (!empty($data['user'])) {
$vars['{%user.fullName%}'] = $data['user']->getFullName();
$vars['{%user.firstName%}'] = $data['user']->getFirstName();
$vars['{%user.phone%}'] = $data['user']->phone;
$vars['{%user.email%}'] = $data['user']->email;
$vars['{%user.jobTitle%}'] = $data['user']->job_title;
$vars['{%user.lastName%}'] = $data['user']->last_name;
$vars['{%user.photoUrl%}'] = $data['user']->getPhotoUrl();
}
$vars = \Eventy::filter('mail_vars.replace', $vars, $data);
if ($escape) {
foreach ($vars as $i => $var) {
$vars[$i] = htmlspecialchars($var ?? '');
$vars[$i] = nl2br($vars[$i]);
}
} else {
foreach ($vars as $i => $var) {
$vars[$i] = nl2br($var ?? '');
}
}
$result = strtr($text, $vars);
// Remove non-replaced placeholders.
if ($remove_non_replaced) {
$result = preg_replace('#\{%[^\.%\}]+\.[^%\}]+%\}#', '', $result ?? '');
$result = trim($result);
}
return $result;
}
/**
* Check if text has vars in it.
*/
public static function hasVars($text)
{
return preg_match('/({%|%})/', $text ?? '');
}
/**
* Remove email from a list of emails.
*/
public static function removeEmailFromArray($list, $email)
{
return array_diff($list, [$email]);
}
/**
* From address for sending system emails.
*/
public static function getSystemMailFrom()
{
$mail_from = Option::get('mail_from');
if (!$mail_from) {
$mail_from = 'freescout@'.\Helper::getDomain();
}
return $mail_from;
}
/**
* Mail driver for sending system emails.
*/
public static function getSystemMailDriver()
{
return Option::get('mail_driver', 'mail');
}
/**
* Send test email from mailbox.
*/
public static function sendTestMail($to, $mailbox = null)
{
if ($mailbox) {
// Configure mail driver according to Mailbox settings
\MailHelper::setMailDriver($mailbox);
$status_message = '';
try {
\Mail::to([$to])->send(new \App\Mail\Test($mailbox));
} catch (\Exception $e) {
// We come here in case SMTP server unavailable for example
$status_message = $e->getMessage();
}
} else {
// System email
\MailHelper::setSystemMailDriver();
$status_message = '';
try {
\Mail::to([['name' => '', 'email' => $to]])
->send(new \App\Mail\Test());
} catch (\Exception $e) {
// We come here in case SMTP server unavailable for example
$status_message = $e->getMessage();
}
}
if (\Mail::failures() || $status_message) {
SendLog::log(null, null, $to, SendLog::MAIL_TYPE_TEST, SendLog::STATUS_SEND_ERROR, null, null, $status_message);
if ($status_message) {
throw new \Exception($status_message, 1);
} else {
return false;
}
} else {
SendLog::log(null, null, $to, SendLog::MAIL_TYPE_TEST, SendLog::STATUS_ACCEPTED);
return true;
}
}
/**
* Check POP3/IMAP connection to the mailbox.
*/
public static function fetchTest($mailbox)
{
$client = \MailHelper::getMailboxClient($mailbox);
// Connect to the Server
$client->connect();
// Get folder
$folder = $client->getFolder('INBOX');
if (!$folder) {
throw new \Exception('Could not get mailbox folder: INBOX', 1);
}
// Get unseen messages for a period
$messages = $folder->query()->unseen()->since(now()->subDays(1))->leaveUnread()->get();
$last_error = '';
if (method_exists($client, 'getLastError')) {
$last_error = $client->getLastError();
}
if ($last_error && stristr($last_error, 'The specified charset is not supported')) {
// Solution for MS mailboxes.
// https://github.com/freescout-helpdesk/freescout/issues/176
$messages = $folder->query()->unseen()->since(now()->subDays(1))->leaveUnread()->setCharset(null)->get();
if (count($client->getErrors()) > 1) {
$last_error = $client->getLastError();
} else {
$last_error = null;
}
}
if ($last_error) {
throw new \Exception($last_error, 1);
} else {
return true;
}
}
/**
* Convert list of emails to array.
*
* @return array
*/
public static function sanitizeEmails($emails)
{
$emails_array = [];
if (is_array($emails)) {
$emails_array = $emails;
} else {
$emails_array = explode(',', $emails ?? '');
}
foreach ($emails_array as $i => $email) {
$emails_array[$i] = \App\Email::sanitizeEmail($email);
if (!$emails_array[$i]) {
unset($emails_array[$i]);
}
}
return $emails_array;
}
/**
* Check if email format is valid.
*
* @param [type] $email [description]
*
* @return [type] [description]
*/
public static function validateEmail($email)
{
return filter_var($email, FILTER_VALIDATE_EMAIL);
}
/**
* Send system alert to super admin.
*/
public static function sendAlertMail($text, $title = '')
{
\App\Jobs\SendAlert::dispatch($text, $title)->onQueue('emails');
}
/**
* Send email to developers team.
*/
public static function sendEmailToDevs($subject, $body, $attachments = [], $from_user = null)
{
// Configure mail driver according to Mailbox settings
\MailHelper::setSystemMailDriver();
$status_message = '';
try {
\Mail::raw($body, function ($message) use ($subject, $attachments, $from_user) {
$message
->subject($subject)
->to(\Config::get('app.freescout_email'));
if ($attachments) {
foreach ($attachments as $attachment) {
$message->attach($attachment);
}
}
// Set user as Reply-To
if ($from_user) {
$message->replyTo($from_user->email, $from_user->getFullName());
}
});
} catch (\Exception $e) {
\Log::error(\Helper::formatException($e));
// We come here in case SMTP server unavailable for example
return false;
}
if (\Mail::failures()) {
return false;
} else {
return true;
}
}
/**
* Get email marker for the outgoing email to track replies
* in case Message-ID header is removed by mail service provider.
*
* @param [type] $message_id [description]
*
* @return [type] [description]
*/
public static function getMessageMarker($message_id)
{
// It has to be BASE64, as Gmail converts it into link.
return '{#FS:'.base64_encode($message_id).'#}';
}
/**
* Fetch Message-ID from incoming email body.
*
* @param [type] $message_id [description]
*
* @return [type] [description]
*/
public static function fetchMessageMarkerValue($body)
{
preg_match('/{#FS:([^#]+)#}/', $body ?? '', $matches);
if (!empty($matches[1]) && base64_decode($matches[1])) {
// Return first found marker.
return base64_decode($matches[1]);
}
return '';
}
public static function getMessageIdHash($thread_id)
{
return substr(md5($thread_id.config('app.key')), 0, 16);
}
/**
* Detect autoresponder by headers.
* https://github.com/jpmckinney/multi_mail/wiki/Detecting-autoresponders
* https://www.jitbit.com/maxblog/18-detecting-outlook-autoreplyout-of-office-emails-and-x-auto-response-suppress-header/.
*
* @return bool [description]
*/
public static function isAutoResponder($headers_str)
{
$autoresponder_headers = [
'x-autoreply' => '',
'x-autorespond' => '',
'auto-submitted' => '', // this can be auto-replied, auto-generated, etc.
'precedence' => ['auto_reply', 'bulk', 'junk'],
'x-precedence' => ['auto_reply', 'bulk', 'junk'],
];
$headers = explode("\n", $headers_str ?? '');
foreach ($autoresponder_headers as $auto_header => $auto_header_value) {
foreach ($headers as $header) {
$parts = explode(':', $header, 2);
if (count($parts) == 2) {
$name = trim(strtolower($parts[0]));
$value = trim($parts[1]);
} else {
continue;
}
if (strtolower($name) == $auto_header) {
if (!$auto_header_value) {
return true;
} elseif (is_array($auto_header_value)) {
foreach ($auto_header_value as $auto_header_value_item) {
if ($value == $auto_header_value_item) {
return true;
}
}
} elseif ($value == $auto_header_value) {
return true;
}
}
}
}
return false;
}
/**
* Check Content-Type header.
* This is not 100% reliable, detects only standard DSN bounces.
*
* @param [type] $headers [description]
*
* @return [type] [description]
*/
public static function detectBounceByHeaders($headers)
{
if (preg_match("/Content-Type:((?:[^\n]|\n[\t ])+)(?:\n[^\t ]|$)/i", $headers, $match)
&& preg_match("/multipart\/report/i", $match[1])
&& preg_match("/report-type=[\"']?delivery-status[\"']?/i", $match[1])
) {
return true;
} else {
return false;
}
}
/**
* Parse email headers.
*
* @param [type] $headers_str [description]
*
* @return [type] [description]
*/
public static function parseHeaders($headers_str)
{
try {
return imap_rfc822_parse_headers($headers_str);
} catch (\Exception $e) {
return;
}
}
public static function getHeader($headers_str, $header)
{
$headers = self::parseHeaders($headers_str);
if (!$headers) {
return;
}
$value = null;
if (property_exists($headers, $header)) {
$value = $headers->$header;
} else {
return;
}
switch ($header) {
case 'message_id':
$value = str_replace(['<', '>'], '', $value);
break;
}
return $value;
}
/**
* Get client for fetching emails.
*/
public static function getMailboxClient($mailbox)
{
$oauth = $mailbox->oauthEnabled();
$new_library = config('app.new_fetching_library');
if (!$oauth && !$new_library) {
return new \Webklex\IMAP\Client([
'host' => $mailbox->in_server,
'port' => $mailbox->in_port,
'encryption' => $mailbox->getInEncryptionName(),
'validate_cert' => $mailbox->in_validate_cert,
'username' => $mailbox->in_username,
'password' => $mailbox->in_password,
'protocol' => $mailbox->getInProtocolName(),
]);
} else {
if ($oauth) {
\Config::set('imap.accounts.default', [
'host' => $mailbox->in_server,
'port' => $mailbox->in_port,
'encryption' => $mailbox->getInEncryptionName(),
'validate_cert' => $mailbox->in_validate_cert,
'username' => $mailbox->email,
'password' => $mailbox->oauthGetParam('a_token'),
'protocol' => $mailbox->getInProtocolName(),
'authentication' => 'oauth',
]);
} else {
\Config::set('imap.accounts.default', [
'host' => $mailbox->in_server,
'port' => $mailbox->in_port,
'encryption' => $mailbox->getInEncryptionName(),
'validate_cert' => $mailbox->in_validate_cert,
// 'username' => $mailbox->email,
// 'password' => $mailbox->oauthGetParam('a_token'),
// 'protocol' => $mailbox->getInProtocolName(),
// 'authentication' => 'oauth',
'username' => $mailbox->in_username,
'password' => $mailbox->in_password,
'protocol' => $mailbox->getInProtocolName(),
]);
}
// To enable debug: /vendor/webklex/php-imap/src/Connection/Protocols
// Debug in console
if (app()->runningInConsole()) {
\Config::set('imap.options.debug', config('app.debug'));
}
$cm = new \Webklex\PHPIMAP\ClientManager(config('imap'));
// Refresh Access Token.
if ($oauth) {
if ((strtotime($mailbox->oauthGetParam('issued_on')) + (int)$mailbox->oauthGetParam('expires_in')) < time()) {
// Try to get an access token (using the authorization code grant)
$token_data = \MailHelper::oauthGetAccessToken(\MailHelper::OAUTH_PROVIDER_MICROSOFT, [
'client_id' => $mailbox->in_username,
'client_secret' => $mailbox->in_password,
'refresh_token' => $mailbox->oauthGetParam('r_token'),
]);
if (!empty($token_data['a_token'])) {
$mailbox->setMetaParam('oauth', $token_data, true);
} elseif (!empty($token_data['error'])) {
$error_message = 'Error occurred refreshing oAuth Access Token: '.$token_data['error'];
\Helper::log(\App\ActivityLog::NAME_EMAILS_FETCHING,
\App\ActivityLog::DESCRIPTION_EMAILS_FETCHING_ERROR, [
'error' => $error_message,
'mailbox' => $mailbox->name,
]);
throw new \Exception($error_message, 1);
}
}
}
// This makes it authenticate two times.
//$cm->setTimeout(60);
return $cm->account('default');
}
}
/**
* Generate artificial Message-ID.
*/
public static function generateMessageId($email_address, $raw_body = '')
{
$hash = str_random(16);
if ($raw_body) {
$hash = md5(strval($raw_body));
}
return 'fs-'.$hash.'@'.preg_replace("/.*@/", '', $email_address);
}
/**
* Fetch IMAP message by Message-ID.
*/
public static function fetchMessage($mailbox, $message_id, $message_date = null)
{
$no_charset = false;
if (!$message_id) {
return null;
}
try {
$client = \MailHelper::getMailboxClient($mailbox);
$client->connect();
} catch (\Exception $e) {
\Helper::logException($e, '('.$mailbox->name.') Could not fetch specific message by Message-ID via IMAP:');
return null;
}
$imap_folders = \Eventy::filter('mail.fetch_message.imap_folders', $mailbox->getInImapFolders(), $mailbox);
foreach ($imap_folders as $folder_name) {
try {
$folder = self::getImapFolder($client, $folder_name);
// Message-ID: <123@123.com>
$query = $folder->query()
->text('<'.$message_id.'>')
->leaveUnread()
->limit(1);
// Limit using date to speed up the search.
if ($message_date) {
$query->since($message_date->subDays(7));
// Here we should add 14 days, as previous line subtracts 7 days.
$query->before($message_date->addDays(14));
}
if ($no_charset) {
$query->setCharset(null);
}
$messages = $query->get();
$last_error = '';
if (method_exists($client, 'getLastError')) {
$last_error = $client->getLastError();
}
if ($last_error && stristr($last_error, 'The specified charset is not supported')) {
// Solution for MS mailboxes.
// https://github.com/freescout-helpdesk/freescout/issues/176
$query = $folder->query()->text('<'.$message_id.'>')->leaveUnread()->limit(1)->setCharset(null);
if ($message_date) {
$query->since($message_date->subDays(7));
$query->before($message_date->addDays(7));
}
$messages = $query->get();
$no_charset = true;
}
if (count($messages)) {
return $messages->first();
}
} catch (\Exception $e) {
\Helper::logException($e, '('.$mailbox->name.') Could not fetch specific message by Message-ID via IMAP:');
}
}
return null;
}
public static function oauthGetAuthorizationUrl($provider_code, $params)
{
$args = [];
switch ($provider_code) {
case self::OAUTH_PROVIDER_MICROSOFT:
// https://docs.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth
$args = [
'scope' => 'offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send',
'response_type' => 'code',
'approval_prompt' => 'auto',
'redirect_uri' => route('mailboxes.oauth_callback'),
];
$args = array_merge($args, $params);
$url = 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?'.http_build_query($args);
break;
}
return $url;
}
public static function oauthGetAccessToken($provider_code, $params)
{
$token_data = [];
$post_params = [];
switch ($provider_code) {
case self::OAUTH_PROVIDER_MICROSOFT:
$post_params = [
'scope' => 'offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send',
"grant_type" => "authorization_code",
'redirect_uri' => route('mailboxes.oauth_callback'),
];
$post_params = array_merge($post_params, $params);
// Refreshing Access Token.
if (!empty($post_params['refresh_token'])) {
$post_params['grant_type'] = 'refresh_token';
}
// $postUrl = "/common/oauth2/token";
// $hostname = "login.microsoftonline.com";
$full_url = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
// $headers = array(
// // "POST " . $postUrl . " HTTP/1.1",
// // "Host: login.windows.net",
// "Content-type: application/x-www-form-urlencoded",
// );
$curl = curl_init($full_url);
curl_setopt($curl, CURLOPT_POST, true);
//curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
curl_setopt($curl, CURLOPT_POSTFIELDS, $post_params);
curl_setopt($curl, CURLOPT_HTTPHEADER, array("application/x-www-form-urlencoded"));
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
\Helper::setCurlDefaultOptions($curl);
curl_setopt($curl, CURLOPT_TIMEOUT, 180);
$response = curl_exec($curl);
if ($response) {
$result = json_decode($response, true);
// [token_type] => Bearer
// [scope] => IMAP.AccessAsUser.All offline_access SMTP.Send User.Read
// [expires_in] => 4514
// [ext_expires_in] => 4514
// [expires_on] => 1646122657
// [not_before] => 1646117842
// [resource] => 00000002-0000-0000-c000-000000000000
// [access_token] => dd
// [refresh_token] => dd
// [id_token] => dd
if (!empty($result['access_token'])) {
$token_data['provider'] = self::OAUTH_PROVIDER_MICROSOFT;
$token_data['a_token'] = $result['access_token'];
$token_data['r_token'] = $result['refresh_token'];
//$token_data['id_token'] = $result['id_token'];
$token_data['issued_on'] = now()->toDateTimeString();
$token_data['expires_in'] = $result['expires_in'];
} elseif ($response) {
$token_data['error'] = $response;
} else {
$token_data['error'] = 'Response code: '.curl_getinfo($curl, CURLINFO_HTTP_CODE);
}
}
curl_close($curl);
break;
}
return $token_data;
}
public static function oauthDisconnect($provider_code, $redirect_uri)
{
switch ($provider_code) {
case self::OAUTH_PROVIDER_MICROSOFT:
return redirect()->away('https://login.microsoftonline.com/common/oauth2/v2.0/logout?post_logout_redirect_uri='.urlencode($redirect_uri));
break;
}
}
public static function prepareMailable($mailable)
{
$custom_headers_str = config('app.custom_mail_headers');
if (empty($custom_headers_str)) {
return;
}
$custom_headers = explode(';', $custom_headers_str);
$mailable->withSwiftMessage(function ($swiftmessage) use ($custom_headers) {
$headers = $swiftmessage->getHeaders();
foreach ($custom_headers as $custom_header) {
$header_parts = explode(':', $custom_header);
$header_name = trim($header_parts[0] ?? '');
$header_value = trim($header_parts[1] ?? '');
if ($header_name && $header_value) {
$headers->addTextHeader($header_name, $header_value);
}
}
return $swiftmessage;
});
}
public static function getImapFolder($client, $folder_name)
{
// https://github.com/freescout-helpdesk/freescout/issues/3502
$folder_name = mb_convert_encoding($folder_name, "UTF7-IMAP","UTF-8");
if (method_exists($client, 'getFolderByPath')) {
return $client->getFolderByPath($folder_name);
} else {
return $client->getFolder($folder_name);
}
}
/**
* This function is used to decode email subjects and attachment names in Webklex libraries.
*/
public static function decodeSubject($subject)
{
// Remove new lines as iconv_mime_decode() may loose a part separated by new line:
// =?utf-8?Q?Gesch=C3=A4ftskonto?= erstellen =?utf-8?Q?f=C3=BCr?=
// 249143
$subject = preg_replace("/[\r\n]/", '', $subject);
// https://github.com/freescout-helpdesk/freescout/issues/3185
$subject = str_ireplace('=?iso-2022-jp?', '=?iso-2022-jp-ms?', $subject);
// Sometimes imap_utf8() can't decode the subject, for example:
// =?iso-2022-jp?B?GyRCIXlCaBsoQjEzMhskQjlmISEhViUsITwlRyVzGyhCJhskQiUoJS8lOSVGJWolIiFXQGxMZ0U5JE4kPyRhJE4jURsoQiYbJEIjQSU1JW0lcyEhIVo3bjQpJSglLyU5JUYlaiUiISYlbyE8JS8hWxsoQg==?=
// and sometimes iconv_mime_decode() can't decode the subject.
// So we are using both.
//
// We are trying iconv_mime_decode() first because imap_utf8()
// decodes umlauts into two symbols:
// https://github.com/freescout-helpdesk/freescout/issues/2965
// Sometimes subject is split into parts and each part is base63 encoded.
// And sometimes it's first encoded and after that split.
// https://github.com/freescout-helpdesk/freescout/issues/3066
// Step 1. Abnormal way - text is encoded and split into parts.
// Only one type of encoding should be used.
preg_match_all("/(=\?[^\?]+\?[BQ]\?)([^\?]+)(\?=)/i", $subject, $m);
$encodings = $m[1] ?? [];
array_walk($encodings, function($value) {
$value = strtolower($value);
});
$one_encoding = count(array_unique($encodings)) == 1;
if ($one_encoding) {
// First try to join all lines and parts.
// Keep in mind that there can be non-encoded parts also:
// =?utf-8?Q?Gesch=C3=A4ftskonto?= erstellen =?utf-8?Q?f=C3=BCr?=
preg_match_all("/(=\?[^\?]+\?[BQ]\?)([^\?]+)(\?=)[\r\n\t ]*/i", $subject, $m);
$joined_parts = '';
if (count($m[1]) > 1 && !empty($m[2]) && !preg_match("/[\r\n\t ]+[^=]/i", $subject)) {
// Example: GyRCQGlNVTtZRTkhIT4uTlMbKEI=
$joined_parts = $m[1][0].implode('', $m[2]).$m[3][0];
// Base64 and URL encoded string can't contain "=" in the middle
// https://stackoverflow.com/questions/6916805/why-does-a-base64-encoded-string-have-an-sign-at-the-end
$has_equal_in_the_middle = preg_match("#=+([^$\? =])#", $joined_parts);
if (!$has_equal_in_the_middle) {
$subject_decoded = iconv_mime_decode($joined_parts, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, "UTF-8");
if ($subject_decoded
&& trim($subject_decoded) != trim($joined_parts)
&& trim($subject_decoded) != trim(rtrim($joined_parts, '='))
&& !self::isNotYetFullyDecoded($subject_decoded)
) {
return $subject_decoded;
}
// Try imap_utf8().
// =?iso-2022-jp?B?IBskQiFaSEcyPDpuQ?= =?iso-2022-jp?B?C4wTU1qIVs3Mkp2JSIlLyU3JSItahsoQg==?=
$subject_decoded = \imap_utf8($joined_parts);
if ($subject_decoded
&& trim($subject_decoded) != trim($joined_parts)
&& trim($subject_decoded) != trim(rtrim($joined_parts, '='))
&& !self::isNotYetFullyDecoded($subject_decoded)
) {
return $subject_decoded;
}
}
}
}
// Step 2. Standard way - each part is encoded separately.
// iconv_mime_decode() can't decode:
// =?iso-2022-jp?B?IBskQiFaSEcyPDpuQC4wTU1qIVs3Mkp2JSIlLyU3JSItahsoQg==?=
$subject_decoded = iconv_mime_decode($subject, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, "UTF-8");
// Sometimes iconv_mime_decode() can't decode some parts of the subject:
// =?iso-2022-jp?B?IBskQiFaSEcyPDpuQC4wTU1qIVs3Mkp2JSIlLyU3JSItahsoQg==?=
// =?iso-2022-jp?B?GyRCQGlNVTtZRTkhIT4uTlMbKEI=?=
if (self::isNotYetFullyDecoded($subject_decoded)) {
$subject_decoded = \imap_utf8($subject);
}
// All previous functions could not decode text.
// mb_decode_mimeheader() properly decodes umlauts into one unice symbol.
// But we use mb_decode_mimeheader() as a last resort as it may garble some symbols.
// Example: =?ISO-8859-1?Q?Vorgang 538336029: M=F6chten Sie Ihre E-Mail-Adresse =E4ndern??=
if (self::isNotYetFullyDecoded($subject_decoded)) {
$subject_decoded = mb_decode_mimeheader($subject);
}
if (!$subject_decoded) {
$subject_decoded = $subject;
}
return $subject_decoded;
}
public static function isNotYetFullyDecoded($subject_decoded) {
// https://stackoverflow.com/questions/15276191/why-does-a-diamond-with-a-questionmark-in-it-appear-in-my-html
$invalid_utf_symbols = ['�'];
return preg_match_all("/=\?[^\?]+\?[BQ]\?/i", $subject_decoded)
|| !mb_check_encoding($subject_decoded, 'UTF-8')
|| \Str::contains($subject_decoded, $invalid_utf_symbols);
}
// public static function oauthGetProvider($provider_code, $params)
// {
// $provider = null;
// switch ($provider_code) {
// case self::OAUTH_PROVIDER_MICROSOFT:
// $provider = new \Stevenmaguire\OAuth2\Client\Provider\Microsoft([
// // Required
// 'clientId' => $params['client_id'],
// 'clientSecret' => $params['client_secret'],
// 'redirectUri' => route('mailboxes.oauth_callback'),
// //https://login.microsoftonline.com/common/oauth2/authorize';
// 'urlAuthorize' => 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
// 'urlAccessToken' => 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
// 'urlResourceOwnerDetails' => 'https://outlook.office.com/api/v1.0/me'
// ]);
// break;
// }
// return $provider;
// }
}