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. '
', // Gmail '
', // Outlook / Live / Hotmail / Microsoft '
[^<]*

/', // 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; // } }