freescout/freescout-dist/app/Misc/Mail.php

1045 lines
38 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Misc;
use App\Mailbox;
use App\Option;
use App\SendLog;
use Webklex\IMAP\Client;
// todo: rename into MailHelper
class Mail
{
/**
* Reply separators.
*/
const REPLY_SEPARATOR_HTML = 'fsReplyAbove';
const REPLY_SEPARATOR_TEXT = '-- Please reply above this line --';
const REPLY_SEPARATOR_NOTIFICATION = 'fsNotifReplyAbove';
/**
* Message-ID prefixes for outgoing emails.
*/
const MESSAGE_ID_PREFIX_NOTIFICATION = 'notify';
const MESSAGE_ID_PREFIX_NOTIFICATION_IN_REPLY = 'conversation';
const MESSAGE_ID_PREFIX_REPLY_TO_CUSTOMER = 'reply';
const MESSAGE_ID_PREFIX_AUTO_REPLY = 'autoreply';
/**
* Mail drivers.
*/
const MAIL_DRIVER_MAIL = 'mail';
const MAIL_DRIVER_SENDMAIL = 'sendmail';
const MAIL_DRIVER_SMTP = 'smtp';
/**
* Encryptions.
*/
const MAIL_ENCRYPTION_NONE = '';
const MAIL_ENCRYPTION_SSL = 'ssl';
const MAIL_ENCRYPTION_TLS = 'tls';
const FETCH_SCHEDULE_EVERY_MINUTE = 1;
const FETCH_SCHEDULE_EVERY_TWO_MINUTES = 2;
const FETCH_SCHEDULE_EVERY_THREE_MINUTES = 3;
const FETCH_SCHEDULE_EVERY_FIVE_MINUTES = 5;
const FETCH_SCHEDULE_EVERY_TEN_MINUTES = 10;
const FETCH_SCHEDULE_EVERY_FIFTEEN_MINUTES = 15;
const FETCH_SCHEDULE_EVERY_THIRTY_MINUTES = 30;
const FETCH_SCHEDULE_HOURLY = 60;
const OAUTH_PROVIDER_MICROSOFT = 'ms';
/**
* If reply is not extracted properly from the incoming email, add here a new separator.
* Order is not important.
* Idially separators must contain < or > 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.
'<div class="gmail_quote">', // Gmail
'<div id="appendonsend"></div>', // Outlook / Live / Hotmail / Microsoft
'<div name="quote" ',
'yahoo_quoted_', // Yahoo, full: <div id=3D"ydp6h4f5c59yahoo_quoted_2937493705"
'------------------ 原始邮件 ------------------', // QQ
'------------------ Original ------------------', // QQ English
'<div id=3D"divRplyFwdMsg" dir=', // Outlook
'regex:/<div style="border:none;border\-top:solid \#[A-Z0-9]{6} 1\.0pt;padding:3\.0pt 0in 0in 0in">[^<]*<p class="MsoNormal"><b>/', // MS Outlook
// General separators.
'regex:/<blockquote((?!quote)[^>])*>/', // General sepator. Should skip Gmail's <blockquote class="gmail_quote">.
'<!-- originalMessage -->',
' 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 = ['<27>'];
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;
// }
}