1086 lines
36 KiB
PHP
1086 lines
36 KiB
PHP
<?php
|
|
/**
|
|
* class.mail.php
|
|
*
|
|
* osTicket Laminas/Mail Wrapper and Mail/Auth Utils & Helpers
|
|
*
|
|
* @author Peter Rotich <peter@osticket.com>
|
|
* @copyright Copyright (c) osTicket <gpl@osticket.com>
|
|
*
|
|
*/
|
|
|
|
// osTicket/Mail namespace
|
|
namespace osTicket\Mail {
|
|
use osTicket\OAuth2\AccessToken;
|
|
// Exception as Mail\RuntimeException
|
|
use Laminas\Mail;
|
|
class Exception extends Mail\Exception\RuntimeException { }
|
|
|
|
// Message
|
|
use Laminas\Mail\Message as MailMessage;
|
|
use Laminas\Mime\Message as MimeMessage;
|
|
use Laminas\Mime\Mime;
|
|
use Laminas\Mime\Part as MimePart;
|
|
use Laminas\Mail\Header;
|
|
use osTicket\Mail\Header\ReturnPath;
|
|
|
|
class Message extends MailMessage {
|
|
// Message Id (mid)
|
|
private $mid;
|
|
// MimeMessage Parts
|
|
private $mimeParts = null;
|
|
// MimeMessage Content
|
|
private $mimeContent = null;
|
|
// Default Charset
|
|
protected $charset = 'utf-8';
|
|
// Default Encoding (upstream is ASCII)
|
|
protected $encoding = 'utf-8';
|
|
|
|
// Internal flags used to set Content-Type
|
|
private $hasHtml = false;
|
|
private $hasAttachments = false;
|
|
private $hasInlineImages = false;
|
|
|
|
public function hasAttachments() {
|
|
return $this->hasAttachments;
|
|
}
|
|
|
|
public function hasInlineImages() {
|
|
return $this->hasInlineImages;
|
|
}
|
|
|
|
public function hasHtml() {
|
|
return $this->hasHtml;
|
|
}
|
|
// Files either attached or inline
|
|
public function hasFiles() {
|
|
return ($this->hasAttachments() || $this->hasInlineImages());
|
|
}
|
|
|
|
public function getId() {
|
|
if (!isset($this->mid)
|
|
&& ($header=$this->getHeader('message-id')))
|
|
$this->mid = $header->getId();
|
|
return $this->mid;
|
|
}
|
|
|
|
public function getMimeMessageParts() {
|
|
if (!isset($this->mimeParts))
|
|
$this->mimeParts = new MimeMessage();
|
|
|
|
return $this->mimeParts;
|
|
}
|
|
|
|
public function getMimeMessageContent() {
|
|
if (!isset($this->mimeContent))
|
|
$this->mimeContent = new ContentMimeMessage();
|
|
|
|
return $this->mimeContent;
|
|
}
|
|
|
|
public function getHeader(string $name) {
|
|
return $this->getHeaders()->getHeader($name);
|
|
}
|
|
|
|
public function addHeader($header, $value=null) {
|
|
if (isset($value))
|
|
$this->getHeaders()->addHeaderLine($header, $value);
|
|
else
|
|
$this->getHeaders()->addHeader($header);
|
|
}
|
|
|
|
public function addHeaders(array $headers) {
|
|
foreach ($headers as $k => $v)
|
|
$this->addHeader($k, $v);
|
|
}
|
|
|
|
private function addMimePart(MimePart $part) {
|
|
$this->getMimeMessageParts()->addPart($part);
|
|
}
|
|
|
|
private function addMimeContent(MimePart $part) {
|
|
$this->getMimeMessageContent()->addPart($part);
|
|
}
|
|
|
|
public function setTextBody($text, $encoding=false) {
|
|
$part = new MimePart($text);
|
|
$part->type = Mime::TYPE_TEXT;
|
|
$part->charset = $this->charset;
|
|
$part->encoding = $encoding;
|
|
$this->addMimeContent($part);
|
|
}
|
|
|
|
public function setHtmlBody($html, $encoding=false) {
|
|
$part = new MimePart($html);
|
|
$part->type = Mime::TYPE_HTML;
|
|
$part->charset = $this->charset;
|
|
$part->encoding = $encoding ?: Mime::ENCODING_BASE64;
|
|
$this->addMimeContent($part);
|
|
$this->hasHtml = true;
|
|
}
|
|
|
|
public function addInlineImage($id, $file) {
|
|
$f = new MimePart($file->getData());
|
|
$f->id = $id;
|
|
$f->type = sprintf('%s; name="%s"',
|
|
$file->getMimeType(),
|
|
$file->getName());
|
|
$f->filename = $file->getName();
|
|
$f->disposition = Mime::DISPOSITION_INLINE;
|
|
$f->encoding = Mime::ENCODING_BASE64;
|
|
$this->addMimePart($f);
|
|
$this->hasInlineImages = true;
|
|
}
|
|
|
|
public function addAttachment($file, $name=null) {
|
|
$f = new MimePart($file->getData());
|
|
$f->type = $file->getMimeType();
|
|
$f->filename = $name ?: $file->getName();
|
|
$f->disposition = Mime::DISPOSITION_ATTACHMENT;
|
|
$f->encoding = Mime::ENCODING_BASE64;
|
|
$this->addMimePart($f);
|
|
$this->hasAttachments = true;
|
|
}
|
|
|
|
// Expects a valid date e.g date('r')
|
|
public function setDate(string $date) {
|
|
$d = new Header\Date($date);
|
|
// Laminas auto adds Date upstream when any header is added
|
|
// We're clearing it here to we back that-date up like it's
|
|
// 99 & 2000 ~ Juvenile
|
|
$this->getHeaders()->removeHeader('date');
|
|
$this->addHeader($d);
|
|
}
|
|
|
|
// Please use this method to set Message-Id otherwise it will be
|
|
// utf-8 endcoded and results is an invalid email & bounces
|
|
public function setMessageId(string $id) {
|
|
try {
|
|
$header = new Header\MessageId();
|
|
$header->setId($id);
|
|
$this->addHeader($header);
|
|
$this->mid = $header->getId();
|
|
} catch (\Throwable $t) {
|
|
// Ignore invalid mid. Upstream will generate one
|
|
}
|
|
}
|
|
|
|
public function getMessageId() {
|
|
return $this->getId();
|
|
}
|
|
|
|
// Valid email address required or no return "<>" tag
|
|
public function setReturnPath($email) {
|
|
try {
|
|
// Exception is thrown on invalid email address
|
|
$header = new ReturnPath();
|
|
$header->addAddress($email);
|
|
$this->getHeaders()->removeHeader($header->getType());
|
|
$this->addHeader($header);
|
|
} catch (\Throwable $t) {
|
|
// It's not email - perhaps it's a tag?
|
|
if (!strcmp($email, '<>'))
|
|
$this->addHeader($header->getFieldName(), $email);
|
|
// Silently dropping the invalid path
|
|
}
|
|
}
|
|
|
|
public function addInReplyTo($inReplyTo) {
|
|
if (!is_array($inReplyTo))
|
|
$inReplyTo = explode(' ', $inReplyTo);
|
|
try {
|
|
$header = new Header\InReplyTo();
|
|
$header->setIds($inReplyTo); #nolint
|
|
$this->addHeader($header);
|
|
} catch (\Throwable $t) {
|
|
// Mshenzi
|
|
}
|
|
}
|
|
|
|
public function addReferences($references) {
|
|
if (!is_array($references))
|
|
$references = explode(' ', $references);
|
|
try {
|
|
$header = new Header\References();
|
|
$header->setIds($references); #nolint
|
|
$this->addHeader($header);
|
|
} catch (\Throwable $t) {
|
|
// Mshenzi
|
|
}
|
|
}
|
|
|
|
// Set Message Sender is useful for SendMail transport, its basically -f
|
|
// parameter in the mail() interface
|
|
public function setSender($email, $name=null) {
|
|
try {
|
|
// Exception is thrown on invalid email address
|
|
$header = new Header\Sender();
|
|
$header->setAddress($email, $name); #nolint
|
|
$this->addHeader($header);
|
|
} catch (\Throwable $t) {
|
|
// Silently ignore invalid email sender defaults to FROM
|
|
// addresses
|
|
}
|
|
}
|
|
|
|
public function setFrom($email, $name=null) {
|
|
// We're resetting the body here when FROM address changes - e.g
|
|
// after failed send attempt while trying multiple SMTP accounts
|
|
unset($this->body);
|
|
return parent::setFrom($email, $name);
|
|
}
|
|
|
|
// This is used to set FROM & and clear Sender to a new Email Address
|
|
public function setOriginator($email, $name=null) {
|
|
// Set the FROM Header
|
|
$this->setFrom($email, $name);
|
|
// Remove Sender Header
|
|
$this->getHeaders()->removeHeader('sender');
|
|
}
|
|
|
|
public function setContentType($contentType) {
|
|
// We can only set content type for multipart message
|
|
if (isset($this->body)
|
|
&& $this->body->isMultiPart()
|
|
&& $contentType) {
|
|
if (($header=$this->getHeaders()->get('Content-Type')))
|
|
$header->setType($contentType); #nolint
|
|
else
|
|
$this->addHeader('Content-Type', $contentType);
|
|
}
|
|
}
|
|
|
|
public function setBody($body=null) {
|
|
// We're ignoring $body param on purpose - only added for
|
|
// upstream compatibility - local interfaces should use
|
|
// prepare() to set the body
|
|
$body = $this->getMimeMessageContent();
|
|
$contentType = $this->hasHtml()
|
|
? Mime::MULTIPART_ALTERNATIVE
|
|
: Mime::TYPE_TEXT;
|
|
// if we have files (inline images or attachments)
|
|
if ($this->hasFiles()) {
|
|
// Content MimePart
|
|
$content = $body->getContentMimePart();
|
|
// Get attachments parts (inline and files)
|
|
$parts = $this->getMimeMessageParts()->getParts();
|
|
// prepend content part to files parts
|
|
array_unshift($parts, $content);
|
|
// Create a new Mime Message and set parts
|
|
$body = new MimeMessage();
|
|
$body->setParts($parts); #nolint
|
|
// We we only have inline images then content type is related
|
|
// otherwise it's mixed.
|
|
$contentType = $this->hasAttachments()
|
|
? Mime::MULTIPART_MIXED
|
|
: Mime::MULTIPART_RELATED;
|
|
}
|
|
// Set body beaches
|
|
parent::setBody($body);
|
|
// Set the content type
|
|
$this->setContentType($contentType);
|
|
}
|
|
|
|
public function prepare() {
|
|
if (!isset($this->body))
|
|
$this->setBody();
|
|
}
|
|
|
|
}
|
|
|
|
// This is a wrapper class for Mime/Message that generates multipart
|
|
// alternative content when email is multipart
|
|
class ContentMimeMessage extends MimeMessage {
|
|
public function getContent() {
|
|
// unpack content parts to a content mime part
|
|
return $this->generateMessage(); #nolint
|
|
}
|
|
|
|
public function getContentMimePart($type=null) {
|
|
$part = new MimePart($this->getContent()); #nolint
|
|
$part->type = $type ?: Mime::MULTIPART_ALTERNATIVE;
|
|
// Set the alternate content boundary
|
|
$part->setBoundary($this->getMime()->boundary()); #nolint
|
|
// Clear the encoding
|
|
$part->encoding = "";
|
|
return $part;
|
|
}
|
|
}
|
|
|
|
// MailBoxProtocolTrait
|
|
use Laminas\Mail\Protocol\Imap as ImapProtocol;
|
|
use Laminas\Mail\Protocol\Pop3 as Pop3Protocol;
|
|
trait MailBoxProtocolTrait {
|
|
final public function init(AccountSetting $setting) {
|
|
// Attempt to connect to the mail server
|
|
$connect = $setting->getConnectionConfig();
|
|
// Let's go Brandon
|
|
parent::__construct($connect['host'], $connect['port'],
|
|
$connect['ssl'], true);
|
|
// Attempt authentication based on MailBoxAccount settings
|
|
$auth = $setting->getAuthCredentials();
|
|
switch (true) {
|
|
case $auth instanceof BasicAuthCredentials:
|
|
if (!$this->basicAuth($auth->getUsername(), $auth->getPassword()))
|
|
throw new Exception('cannot login, user or password wrong');
|
|
break;
|
|
case $auth instanceof OAuth2AuthCredentials:
|
|
// Get OAuth2 Authentication Request
|
|
$authen = $auth->getAuthRequest($setting->getUser());
|
|
if (!$this->oauth2Auth($authen))
|
|
throw new Exception('OAuth2 Authentication Error');
|
|
break;
|
|
default:
|
|
throw new Exception('Unknown Credentials Type');
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* Basic Authentication (Legacy) for the OG
|
|
*/
|
|
private function basicAuth($username, $password) {
|
|
return $this->login($username, $password);
|
|
}
|
|
|
|
abstract public function __construct($accountSetting);
|
|
abstract protected function oauth2Auth($authen);
|
|
}
|
|
|
|
class ImapMailboxProtocol extends ImapProtocol {
|
|
use MailBoxProtocolTrait;
|
|
public function __construct($accountSetting) {
|
|
$this->init($accountSetting);
|
|
}
|
|
|
|
/*
|
|
* [connection begins]
|
|
* C: C01 CAPABILITY
|
|
* S: * CAPABILITY … AUTH=XOAUTH2
|
|
* S: C01 OK Completed
|
|
* C: A01 AUTHENTICATE XOAUTH2 {XOAUTH2}
|
|
* S: A01 (OK|NO|BAD)
|
|
* [connection continues...]
|
|
*/
|
|
private function oauth2Auth($authen) {
|
|
$this->sendRequest('AUTHENTICATE', ['XOAUTH2', $authen]);
|
|
while (true) {
|
|
$matches = [];
|
|
$response = '';
|
|
if ($this->readLine($response, '+', true)) {
|
|
$this->sendRequest('');
|
|
} elseif (preg_match("/^CAPABILITY /i", $response)) {
|
|
continue;
|
|
} elseif (preg_match("/^OK /i", $response)) {
|
|
return true;
|
|
} elseif (preg_match('/^(NO|BAD) (.*+)$/i',
|
|
$response, $matches)) {
|
|
throw new Exception($matches[2]);
|
|
} else {
|
|
throw new Exception('Unknown Oauth2 Error:
|
|
'.$response);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
}
|
|
|
|
class Pop3MailboxProtocol extends Pop3Protocol {
|
|
use MailBoxProtocolTrait;
|
|
public function __construct($accountSetting) {
|
|
$this->init($accountSetting);
|
|
}
|
|
|
|
/*
|
|
* [connection begins]
|
|
* C: AUTH XOAUTH2
|
|
* S: +
|
|
* C: {XOAUTH2}
|
|
* S: (+OK|-ERR|+ {msg})
|
|
* [connection continues...]
|
|
*/
|
|
public function oauth2Auth($authen) {
|
|
$this->sendRequest('AUTH XOAUTH2');
|
|
while (true) {
|
|
$response = $this->readLine();
|
|
$matches = [];
|
|
if ($response == '+') {
|
|
// Send xOAuthRequest
|
|
$this->sendRequest($authen);
|
|
} elseif (preg_match("/^\+OK /i", $response)) {
|
|
return true;
|
|
} elseif (preg_match('/^-ERR (.*+)$/i',
|
|
$response, $matches)) {
|
|
throw new Exception($matches[2]);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
* readLine
|
|
*
|
|
* Pop3 Protocol doesn't have readLine function and readRresponse
|
|
* has hardcoded status of "+OK" whereas Oauth2 response returns "+"
|
|
* on AUTH XOAUTH2 command.
|
|
*/
|
|
public function readLine() {
|
|
$result = fgets($this->socket);
|
|
if (!is_string($result))
|
|
throw new Exception('read failed - connection closed');
|
|
return trim($result);
|
|
}
|
|
|
|
public function login($user, $password, $tryApop = true) {
|
|
try {
|
|
parent::login($user, $password, $tryApop);
|
|
return true;
|
|
} catch (\Throwable $e) {
|
|
throw new Exception(__('login failed').': '.$e->getMessage());
|
|
}
|
|
}
|
|
}
|
|
|
|
// MailBoxStorageTrait
|
|
use Laminas\Mail\Storage\Imap as ImapStorage;
|
|
use Laminas\Mail\Storage\Pop3 as Pop3Storage;
|
|
use RecursiveIteratorIterator;
|
|
trait MailBoxStorageTrait {
|
|
private $folder;
|
|
private $hostInfo;
|
|
|
|
private function init(AccountSetting $setting) {
|
|
$this->folder = $setting->getAccount()->getFolder();
|
|
$this->hostInfo = $setting->getHostInfo();
|
|
}
|
|
|
|
public function getHostInfo() {
|
|
return $this->hostInfo;
|
|
}
|
|
|
|
private function getFolder() {
|
|
return $this->folder;
|
|
}
|
|
|
|
public function createFolder($name, $parentFolder = null) {
|
|
try {
|
|
parent::createFolder($name, $parentFolder);
|
|
$this->folders = null;
|
|
return true;
|
|
} catch (\Exception $ex) {
|
|
// noop
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public function hasFolder($folder, $rootFolder = null) {
|
|
$folders = $this->getFolders($rootFolder);
|
|
if (is_array($folders)
|
|
&& in_array(strtolower($folder), $folders))
|
|
return true;
|
|
|
|
// Try selecting the folder.
|
|
try {
|
|
$this->selectFolder($folder);
|
|
return true;
|
|
} catch (\Exception $ex) {
|
|
//noop
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public function getFolders($rootFolder = null) {
|
|
if (!isset($this->folders)) {
|
|
$folders = new RecursiveIteratorIterator(
|
|
parent::getFolders(),
|
|
RecursiveIteratorIterator::SELF_FIRST
|
|
);
|
|
$this->folders = [];
|
|
foreach ($folders as $name => $folder) {
|
|
if (!$folder->isSelectable()) #nolint
|
|
continue;
|
|
$this->folders[] = strtolower($folder->getGlobalName()); #nolint
|
|
}
|
|
}
|
|
return $this->folders;
|
|
}
|
|
|
|
/*
|
|
* getRawEmail
|
|
*
|
|
* Given message number - get full raw email (headers + content)
|
|
*
|
|
*/
|
|
public function getRawEmail(int $i) {
|
|
return trim($this->getRawHeader($i)) . "\r\n\r\n" . $this->getRawContent($i);
|
|
}
|
|
|
|
/*
|
|
* move an existing message to a folder
|
|
*
|
|
* Caller should catch possible exception
|
|
*/
|
|
public function moveMessage($i, $folder) {
|
|
parent::moveMessage($i, $folder);
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* Remove a message from server.
|
|
*
|
|
* Caller should catch possible exception
|
|
*/
|
|
public function removeMessage($i) {
|
|
parent::removeMessage($i);
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* markAsSeen
|
|
*/
|
|
public function markAsSeen($i) {
|
|
// noop - storage that implement it should define it
|
|
}
|
|
|
|
public function expunge() {
|
|
// noop - only IMAP
|
|
}
|
|
}
|
|
|
|
// Imap
|
|
use Laminas\Mail\Storage;
|
|
class Imap extends ImapStorage {
|
|
use MailBoxStorageTrait;
|
|
private $folders;
|
|
|
|
public function __construct($accountSetting) {
|
|
$protocol = new ImapMailBoxProtocol($accountSetting);
|
|
parent::__construct($protocol);
|
|
$this->init($accountSetting);
|
|
}
|
|
|
|
// Mark message as seen
|
|
public function markAsSeen($i) {
|
|
try {
|
|
return $this->setFlags($i, [Storage::FLAG_SEEN]);
|
|
} catch (\Throwable $t) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove a message from server without expunging the mailbox
|
|
*
|
|
* Laminas Mail (upstream) auto expunges the mailbox on message
|
|
* removal or move (copy + remove) - which can cause major issues
|
|
* for us since we fetcher uses message sequence numbers to fetch
|
|
* messages / emails.
|
|
*
|
|
* We expunge the mailbox at the end if fetch session.
|
|
*
|
|
* TODO: Make PR upstream to support calling removeMessage with
|
|
* a boolean flag i.e removeMessage(int $id, bool $expunge = true)
|
|
*
|
|
*/
|
|
public function removeMessage($i) {
|
|
if (! $this->protocol->store([Storage::FLAG_DELETED], $i, null, '+')) {
|
|
throw new Exception('cannot set deleted flag');
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Expunge mailbox
|
|
public function expunge() {
|
|
return $this->protocol->expunge();
|
|
}
|
|
}
|
|
|
|
// Pop3
|
|
class Pop3 extends Pop3Storage {
|
|
use MailBoxStorageTrait;
|
|
|
|
public function __construct($accountSetting) {
|
|
$protocol = new Pop3MailboxProtocol($accountSetting);
|
|
parent::__construct($protocol);
|
|
$this->init($accountSetting);
|
|
}
|
|
}
|
|
|
|
// Smtp
|
|
use Laminas\Mail\Transport\Smtp as SmtpTransport;
|
|
class Smtp extends SmtpTransport {
|
|
private $connected = false;
|
|
public function __construct(SmtpOptions $options) {
|
|
parent::__construct($options);
|
|
}
|
|
|
|
private function isConnected() {
|
|
return $this->connected;
|
|
}
|
|
|
|
public function connect() {
|
|
try {
|
|
if (!$this->isConnected() && parent::connect())
|
|
$this->connected = true;
|
|
return $this->isConnected();
|
|
} catch (\Throwable $ex) {
|
|
// Smtp protocol throws an Exception via error handler
|
|
// resulting in unrestored handler on socket error
|
|
restore_error_handler();
|
|
throw $ex;
|
|
}
|
|
}
|
|
|
|
public function sendMessage(Message $message) {
|
|
try {
|
|
// Make sure the body is set
|
|
$message->prepare();
|
|
parent::send($message);
|
|
} catch (\Throwable $ex) {
|
|
$this->connected = false;
|
|
throw $ex;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// SmtpOptions
|
|
use Laminas\Mail\Transport\SmtpOptions as SmtpSettings;
|
|
class SmtpOptions extends SmtpSettings {
|
|
public function __construct(AccountSetting $setting) {
|
|
parent::__construct($this->buildOptions($setting));
|
|
}
|
|
|
|
// Build out SmtpOptions options based on SmtpAccount Settings
|
|
private function buildOptions(AccountSetting $setting) {
|
|
// Dont send 'QUIT' on __destruct()
|
|
$config = [
|
|
'use_complete_quit' => false,
|
|
'novalidatecert' => true
|
|
];
|
|
$connect = $setting->getConnectionConfig();
|
|
$auth = $setting->getAuthCredentials();
|
|
switch (true) {
|
|
case $auth instanceof NoAuthCredentials:
|
|
// No Authentication - simply return host and port
|
|
return [
|
|
'host' => $connect['host'],
|
|
'port' => $connect['port'],
|
|
'name' => $connect['name'],
|
|
];
|
|
break;
|
|
case $auth instanceof BasicAuthCredentials:
|
|
$config += [
|
|
'username' => $auth->getUsername(),
|
|
'password' => $auth->getPasswd(),
|
|
'ssl' => $connect['ssl'],
|
|
];
|
|
break;
|
|
case $auth instanceof OAuth2AuthCredentials:
|
|
$token = $auth->getAccessToken();
|
|
if ($token->hasExpired())
|
|
throw new Exception('Access Token is Expired');
|
|
$config += [
|
|
'xoauth2' => $token->getAuthRequest(),
|
|
'ssl' => $connect['ssl'],
|
|
];
|
|
break;
|
|
default:
|
|
throw new Exception('Unknown Authentication Type');
|
|
}
|
|
|
|
return [
|
|
'host' => $connect['host'],
|
|
'port' => $connect['port'],
|
|
'name' => $connect['name'],
|
|
'connection_time_limit' => 300, # 5 minutes limit
|
|
'connection_class' => $auth->getConnectionClass(),
|
|
'connection_config' => $config
|
|
];
|
|
}
|
|
}
|
|
|
|
// Sendmail
|
|
use Laminas\Mail\Transport\Sendmail as SendmailTransport;
|
|
class Sendmail extends SendmailTransport {
|
|
public function __construct($options) {
|
|
parent::__construct($options);
|
|
}
|
|
|
|
public function sendMessage(Message $message) {
|
|
try {
|
|
// Make sure the body is set
|
|
$message->prepare();
|
|
parent::send($message);
|
|
return true;
|
|
} catch (\Throwable $ex) {
|
|
throw $ex;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Credentials
|
|
abstract class AuthCredentials {
|
|
static $class = 'plain';
|
|
|
|
public function getConnectionClass() {
|
|
return static::$class;
|
|
}
|
|
|
|
public function serialize() {
|
|
return json_encode($this->__serialize());
|
|
}
|
|
|
|
public function __serialize() {
|
|
return $this->toArray();
|
|
}
|
|
|
|
public static function init(array $options) {
|
|
return new static($options);
|
|
}
|
|
|
|
abstract function __construct(array $options);
|
|
abstract function toArray();
|
|
}
|
|
|
|
class NoAuthCredentials extends AuthCredentials {
|
|
private $username;
|
|
|
|
public function __construct(array $options) {
|
|
if (empty($options['username'])) {
|
|
throw new Exception(sprintf(
|
|
__('Required option not passed: "%s"'),
|
|
'username'));
|
|
}
|
|
$this->username = $options['username'];
|
|
}
|
|
|
|
public function getUsername() {
|
|
return $this->username;
|
|
}
|
|
public function toArray() {
|
|
return [
|
|
'username' => $this->getUsername()
|
|
];
|
|
}
|
|
}
|
|
|
|
class BasicAuthCredentials extends AuthCredentials {
|
|
static $class = 'login';
|
|
private $username;
|
|
private $password;
|
|
|
|
public function __construct(array $options) {
|
|
if (empty($options['username'])) {
|
|
throw new Exception(sprintf(
|
|
__('Required option not passed: "%s"'),
|
|
'username'));
|
|
}
|
|
|
|
if (empty($options['password'])) {
|
|
throw new Exception(sprintf(
|
|
__('Required option not passed: "%s"'),
|
|
'password'));
|
|
}
|
|
$this->username = $options['username'];
|
|
$this->password = $options['password'];
|
|
}
|
|
|
|
public function getUsername() {
|
|
return $this->username;
|
|
}
|
|
|
|
public function getPasswd() {
|
|
return $this->getPassword();
|
|
}
|
|
|
|
public function getPassword() {
|
|
return $this->password;
|
|
}
|
|
|
|
public function toArray() {
|
|
return [
|
|
'username' => $this->getUsername(),
|
|
'password' => $this->getPassword()
|
|
];
|
|
}
|
|
}
|
|
|
|
class OAuth2AuthCredentials extends AuthCredentials {
|
|
static $class = 'osTicket\Mail\Protocol\Smtp\Auth\OAuth2';
|
|
private $token;
|
|
public function __construct(array $options) {
|
|
if (empty($options['access_token'])) {
|
|
throw new Exception(sprintf(
|
|
__('Required option not passed: "%s"'),
|
|
'access_token'));
|
|
}
|
|
|
|
if (empty($options['resource_owner_email'])) {
|
|
throw new Exception(sprintf(
|
|
__('Required option not passed: "%s"'),
|
|
'resource_owner_email'));
|
|
}
|
|
$this->token = new AccessToken($options);
|
|
}
|
|
|
|
public function getToken() {
|
|
return $this->token;
|
|
}
|
|
|
|
public function getAuthRequest($user=null) {
|
|
return $this->getToken()
|
|
? $this->getToken()->getAuthRequest($user)
|
|
: null;
|
|
}
|
|
|
|
public function getAccessToken($signature=false) {
|
|
$token = $this->getToken();
|
|
// check signature if requested
|
|
return (!$signature
|
|
|| !strcmp($signature, $token->getConfigSignature()))
|
|
? $token : null;
|
|
}
|
|
|
|
public function toArray() {
|
|
return $this->token->toArray();
|
|
}
|
|
}
|
|
|
|
// osTicket/Mail/AccountSetting
|
|
class AccountSetting {
|
|
private $account;
|
|
private $creds;
|
|
private $connection = [];
|
|
private $errors = [];
|
|
|
|
public function __construct(\EmailAccount $account) {
|
|
// Set the account
|
|
$this->account = &$account;
|
|
// Parse Connection Options
|
|
// We allow scheme to hint for encryption for people using ssl or tls
|
|
// on nonstandard ports.
|
|
$host = $account->getHost();
|
|
$port = (int) $account->getPort();
|
|
$ssl = null;
|
|
$matches = [];
|
|
if (preg_match('~^(ssl|tls|plain)://(.*+)$~iu',
|
|
strtolower($host), $matches)) {
|
|
list(, $ssl, $host) = $matches;
|
|
// Clear ssl when "plain" connection is being forced without
|
|
// using port number as the indicator!
|
|
$ssl = strcmp($ssl, 'plain') ? $ssl : null;
|
|
// Why would someone use a standard encryption based port
|
|
// for unencrypted connection is beyond me - but apparently
|
|
// it's a thing!!
|
|
} elseif (!$ssl && $port) {
|
|
// Set ssl or tls on based on standard ports
|
|
if (in_array($port, [465, 993, 995]))
|
|
$ssl = 'ssl';
|
|
elseif (in_array($port, [587]))
|
|
$ssl = 'tls';
|
|
}
|
|
|
|
// Set the connection settings
|
|
$this->connection = [
|
|
'host' => $host,
|
|
'port' => $port,
|
|
'ssl' => $ssl,
|
|
'protocol' => strtoupper($account->getProtocol()),
|
|
'name' => self::get_hostname(),
|
|
];
|
|
|
|
// Set errors to null to clear validation
|
|
$this->errors = null;
|
|
}
|
|
|
|
public function getUser() {
|
|
return $this->account->getEmail()->getEmail();
|
|
}
|
|
|
|
public function getName() {
|
|
return $this->connection['name'];
|
|
}
|
|
|
|
public function getHost() {
|
|
return $this->connection['host'];
|
|
}
|
|
|
|
public function getPort() {
|
|
return $this->connection['port'];
|
|
}
|
|
|
|
public function getSsl() {
|
|
return $this->connection['ssl'];
|
|
}
|
|
|
|
public function getProtocol() {
|
|
return $this->connection['protocol'];
|
|
}
|
|
|
|
public function setCredentials(AuthCredentials $creds) {
|
|
$this->creds = $creds;
|
|
}
|
|
|
|
public function getCredentials() {
|
|
if (!isset($this->creds))
|
|
$this->creds = $this->account->getCredentials();
|
|
return $this->creds;
|
|
}
|
|
|
|
public function getAuthCredentials() {
|
|
return $this->getCredentials();
|
|
}
|
|
|
|
public function getAccount() {
|
|
return $this->account;
|
|
}
|
|
|
|
public function getConnectionConfig() {
|
|
return $this->connection;
|
|
}
|
|
|
|
public function getHostInfo() {
|
|
return $this->describe();
|
|
}
|
|
|
|
public function asArray() {
|
|
return $this->getConnectionConfig();
|
|
}
|
|
|
|
public function describe() {
|
|
return sprintf('%s://%s:%s/%s',
|
|
$this->getSsl() ?: 'plain',
|
|
$this->getHost(),
|
|
$this->getPort(),
|
|
$this->getProtocol());
|
|
}
|
|
|
|
private function validate() {
|
|
|
|
if (!isset($this->errors)) {
|
|
// Set errors to an array to to make sure don't
|
|
// unneccesarily validate valid info again.
|
|
$this->errors = [];
|
|
// We're simply making sure required info are set. True
|
|
// validation will happen at the protocol connection level
|
|
$info = $this->getConnectionConfig();
|
|
foreach (['host', 'port', 'protocol'] as $p ) {
|
|
if (!isset($info[$p]) || !$info[$p])
|
|
$this->errors[$p] = sprintf('%s %s',
|
|
strtoupper($p), __('Required'));
|
|
}
|
|
}
|
|
return !count($this->errors);
|
|
}
|
|
|
|
public function isValid() {
|
|
return $this->validate();
|
|
}
|
|
|
|
public function getErrors() {
|
|
return $this->errors;
|
|
}
|
|
|
|
/*
|
|
* get_hostname
|
|
*
|
|
* Please note that this is different from getHost above
|
|
*
|
|
* Here we're getting the hostname to use on HELO/EHLO when
|
|
* initiating an SMTP connection. It should be a valid hostname with
|
|
* valid reverse-lookup for better deliverability.
|
|
*
|
|
* Perhaps this can be a setting in the future but allowing users
|
|
* to set it to **anything** will results in more mail issues than just
|
|
* defaulting to what the OS tells us or localhost for that matter.
|
|
*
|
|
* For now, we're simply asking core osTicket to give us the OS hostname
|
|
* otherwise it will default to localhost which some mail servers frawns
|
|
* upon since it won't have a valid reverse-lookup.
|
|
*
|
|
*/
|
|
private static function get_hostname() {
|
|
// We're simply returning what the OS is telling us!
|
|
return php_uname('n');
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace osTicket\Mail\Protocol\Smtp\Auth {
|
|
// Exception as Mail\RuntimeException
|
|
use Laminas\Mail;
|
|
class Exception extends Mail\Exception\RuntimeException { }
|
|
use Laminas\Mail\Protocol\Smtp;
|
|
class OAuth2 extends Smtp {
|
|
private $xoauth2;
|
|
public function __construct($host = null, $port = null, $config = null) {
|
|
$this->setParams($host, $config);
|
|
parent::__construct($host, $port, $config);
|
|
}
|
|
|
|
private function setParams($host, $config) {
|
|
$_config = [];
|
|
if (is_array($host))
|
|
$_config = is_array($config)
|
|
? array_replace_recursive($host, $config)
|
|
: $host;
|
|
if (is_array($_config) && isset($_config['xoauth2']))
|
|
$this->xoauth2 = $_config['xoauth2'];
|
|
}
|
|
|
|
private function getAuthRequest() {
|
|
return $this->xoauth2;
|
|
}
|
|
|
|
/*
|
|
* [connection begins]
|
|
* C: auth xoauth2
|
|
* S: 334
|
|
* C: {XOAUTH2}
|
|
* S: (235|XXX)
|
|
* [connection continues...]
|
|
*/
|
|
public function auth() {
|
|
// Check Parent
|
|
parent::auth();
|
|
// Make sure we have XOAUTH2
|
|
if (!($xoauth2=$this->getAuthRequest()))
|
|
throw new Exception('XOAUTH2 Required');
|
|
$this->_send('AUTH XOAUTH2');
|
|
$this->_expect(334);
|
|
$this->_send($xoauth2);
|
|
$this->_expect(235);
|
|
$this->auth = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace osTicket\Mail\Header {
|
|
use Laminas\Mail\Header\AbstractAddressList;
|
|
use Laminas\Mail\Header\HeaderInterface;
|
|
use Laminas\Mail\Address;
|
|
|
|
class ReturnPath extends AbstractAddressList {
|
|
protected $fieldName = 'Return-Path';
|
|
protected static $type = 'return-path';
|
|
|
|
public function addAddress($email) {
|
|
$this->getAddressList()->add(new Address($email)); #nolint
|
|
}
|
|
|
|
public function getFieldValue($format = HeaderInterface::FORMAT_RAW) {
|
|
// We're simply intercepting Value here to add <> to the email
|
|
return sprintf('<%s>', parent::getFieldValue($format));
|
|
}
|
|
|
|
public function getType() {
|
|
return self::$type;
|
|
}
|
|
}
|
|
}
|