osticket-docker/osTicket-v1.18.2/include/class.email.php

1475 lines
49 KiB
PHP

<?php
/*********************************************************************
class.email.php
Peter Rotich <peter@osticket.com>
Copyright (c) 2006-2013 osTicket
http://www.osticket.com
Released under the GNU General Public License WITHOUT ANY WARRANTY.
See LICENSE.TXT for details.
vim: expandtab sw=4 ts=4 sts=4:
**********************************************************************/
require_once(INCLUDE_DIR.'laminas-mail/vendor/autoload.php');
include_once INCLUDE_DIR.'class.role.php';
include_once(INCLUDE_DIR.'class.dept.php');
include_once(INCLUDE_DIR.'class.mail.php');
include_once(INCLUDE_DIR.'class.mailer.php');
include_once(INCLUDE_DIR.'class.oauth2.php');
include_once(INCLUDE_DIR.'class.mailfetch.php');
include_once(INCLUDE_DIR.'class.mailparse.php');
include_once(INCLUDE_DIR.'api.tickets.php');
class Email extends VerySimpleModel {
static $meta = array(
'table' => EMAIL_TABLE,
'pk' => array('email_id'),
'joins' => array(
'priority' => array(
'constraint' => array('priority_id' => 'Priority.priority_id'),
'null' => true,
),
'dept' => array(
'constraint' => array('dept_id' => 'Dept.id'),
'null' => true,
),
'topic' => array(
'constraint' => array('topic_id' => 'Topic.topic_id'),
'null' => true,
),
'mailbox' => array(
'reverse' => 'MailBoxAccount.account',
'list' => false,
'null' => true,
),
'smtp' => array(
'reverse' => 'SmtpAccount.account',
'list' => false,
'null' => true,
),
)
);
const PERM_BANLIST = 'emails.banlist';
static protected $perms = array(
self::PERM_BANLIST => array(
'title' =>
/* @trans */ 'Banlist',
'desc' =>
/* @trans */ 'Ability to add/remove emails from banlist via ticket interface',
'primary' => true,
));
private $stash;
private $address;
function getId() {
return $this->email_id;
}
function __toString() {
return $this->getAddress();
}
// TODO: move stash/restore to a StashableTrait
function restore($key, $drop=true) {
if (($data = $this->stash($key, null)) && $drop)
$this->stash[$key] = null;
return $data;
}
function stash($key, $data=null) {
if (!isset($this->stash))
$this->stash = &$_SESSION[':email'][$this->getId()];
// If data is null then stash is being pop-ed
if (!isset($data) && $key && isset($this->stash[$key]))
return $this->stash[$key];
// stash data
if ($key && $data)
$this->stash[$key] = $data;
}
function stashFormData(array $data) {
$this->stash('formdata', array_filter($data));
}
function restoreFormData($drop=true) {
return $this->restore('formdata', $drop) ?: [];
}
function restoreErrors($drop=true) {
return $this->restore('errors', $drop) ?: [];
}
function restoreNotice($drop=true) {
return $this->restore('notice', $drop);
}
function getEmail() {
return $this->email;
}
function getAddress() {
if (!isset($this->address))
$this->address = $this->name
? sprintf('%s <%s>', $this->name, $this->email)
: $this->email;
return $this->address;
}
function getName() {
return $this->name;
}
function getPriorityId() {
return $this->priority_id;
}
function getDeptId() {
return $this->dept_id;
}
function getDept() {
return $this->dept;
}
function getTopicId() {
return $this->topic_id;
}
function getTopic() {
return $this->topic;
}
function autoRespond() {
return !$this->noautoresp;
}
function getHashtable() {
return $this->ht;
}
static function getSupportedAuthTypes() {
static $auths = null;
if (!isset($auths)) {
$auths = [];
// OAuth auth
foreach (Oauth2AuthorizationBackend::allRegistered() as $id => $bk)
$auths[$id] = $bk->getName();
// Basic authentication
$auths['basic'] = sprintf('%s (%s)',
__('Basic Authentication'),
__('Legacy'));
}
return $auths;
}
static function getSupportedSMTPAuthTypes() {
return array_merge([
'mailbox' => sprintf('%s %s',
__('Same as'), __('Remote Mailbox')),
'none' => sprintf('%s - %s',
__('None'), __('No Authentication Required'))],
self::getSupportedAuthTypes());
}
function getInfo() {
// Base information mimus objects
$info = array_filter($this->getHashtable(), function($e) {
return !is_object($e);
});
// Remote Mailbox Info
if (($mailbox=$this->getMailBoxAccount()))
$info = array_merge($info, $mailbox->getInfo());
// SMTP Account Info
if (($smtp=$this->getSmtpAccount()))
$info = array_merge($info, $smtp->getInfo());
// Restore stahed formdata (if any)
if ($_SERVER['REQUEST_METHOD'] == 'GET'
&& ($data=$this->restoreFormData()))
$info = array_merge($info, $data);
return $info;
}
function getMailBoxAccount($autoinit=true) {
if (!$this->mailbox && isset($this->email_id) && $autoinit)
$this->mailbox = MailBoxAccount::create([
'email_id' => $this->email_id]);
return $this->mailbox;
}
function getSmtpAccount($autoinit=true) {
if (!$this->smtp && isset($this->email_id) && $autoinit)
$this->smtp = SmtpAccount::create([
'email_id' => $this->email_id]);
return $this->smtp;
}
function getAuthAccount($which) {
$account = null;
switch ($which) {
case 'mailbox':
$account = $this->getMailBoxAccount();
break;
case 'smtp':
$account = $this->getSmtpAccount();
break;
}
return $account;
}
function send($to, $subject, $message, $attachments=null, $options=null, $cc=array()) {
$mailer = new osTicket\Mail\Mailer($this);
if($attachments)
$mailer->addAttachments($attachments);
return $mailer->send($to, $subject, $message, $options, $cc);
}
function sendAutoReply($to, $subject, $message, $attachments=null, $options=array()) {
$options+= array('autoreply' => true);
return $this->send($to, $subject, $message, $attachments, $options);
}
function sendAlert($to, $subject, $message, $attachments=null, $options=array()) {
$options+= array('notice' => true);
return $this->send($to, $subject, $message, $attachments, $options);
}
function delete() {
global $cfg;
//Make sure we are not trying to delete default emails.
if(!$cfg || $this->getId()==$cfg->getDefaultEmailId() || $this->getId()==$cfg->getAlertEmailId()) //double...double check.
return 0;
if (!parent::delete())
return false;
$type = array('type' => 'deleted');
Signal::send('object.deleted', $this, $type);
// Delete email accounts
if ($this->mailbox)
$this->mailbox->delete();
if ($this->smtp)
$this->smtp->delete();
Dept::objects()
->filter(array('email_id' => $this->getId()))
->update(array(
'email_id' => $cfg->getDefaultEmailId()
));
Dept::objects()
->filter(array('autoresp_email_id' => $this->getId()))
->update(array(
'autoresp_email_id' => 0,
));
return true;
}
function save($refetch=false) {
if ($this->dirty)
$this->updated = SqlFunction::NOW();
return parent::save($refetch || $this->dirty);
}
function update($vars, &$errors=false) {
global $cfg;
// very basic checks
$vars['name'] = Format::striptags(trim($vars['name']));
$vars['email'] = trim($vars['email']);
$id = isset($this->email_id) ? $this->getId() : 0;
if ($id && $id != $vars['id'])
$errors['err']=__('Get technical help!')
.' '.__('Internal error occurred');
if (!$vars['email'] || !Validator::is_email($vars['email'])) {
$errors['email']=__('Valid email required');
} elseif (($eid=Email::getIdByEmail($vars['email'])) && $eid != $id) {
$errors['email']=__('Email already exists');
} elseif ($cfg && !strcasecmp($cfg->getAdminEmail(), $vars['email'])) {
$errors['email']=__('Email already used as admin email!');
} elseif (Staff::getIdByEmail($vars['email'])) { //make sure the email doesn't belong to any of the staff
$errors['email']=__('Email in use by an agent');
}
if (!$vars['name'])
$errors['name']=__('Email name required');
/*
TODO: ???
$dept = Dept::lookup($vars['dept_id']);
if($dept && !$dept->isActive())
$errors['dept_id'] = '';
$topic = Topic::lookup($vars['topic_id']);
if($topic && !$topic->isActive())
$errors['topic_id'] = '';
*/
// Remote Mailbox Settings
if (($mailbox = $this->getMailBoxAccount()))
$mailbox->update($vars, $errors);
// SMTP Settings
if (($smtp = $this->getSmtpAccount()))
$smtp->update($vars, $errors);
//abort on errors
if ($errors)
return false;
if ($errors) return false;
// Update basic settings
$this->email = Format::sanitize($vars['email']);
$this->name = Format::striptags($vars['name']);
$this->dept_id = (int) $vars['dept_id'];
$this->priority_id = (int) (isset($vars['priority_id']) ? $vars['priority_id'] : 0);
$this->topic_id = (int) $vars['topic_id'];
$this->noautoresp = (int) $vars['noautoresp'];
$this->notes = Format::sanitize($vars['notes']);
if ($this->save())
return true;
if ($id) { //update
$errors['err'] = sprintf(__('Unable to update %s.'), __('this email'))
.' '.__('Internal error occurred');
} else {
$errors['err'] = sprintf(__('Unable to add %s.'), __('this email'))
.' '.__('Internal error occurred');
}
return false;
}
static function getIdByEmail($email) {
$qs = static::objects()->filter(Q::any(array(
'email' => $email,
)))
->values_flat('email_id');
$row = $qs->first();
return $row ? $row[0] : false;
}
static function create($vars=false) {
$inst = new static($vars);
$inst->created = SqlFunction::NOW();
return $inst;
}
static function getAddresses($options=array(), $flat=true) {
$objects = static::objects();
if ($options['smtp'])
$objects = $objects->filter(array('smtp__active' => 1));
if ($options['depts'])
$objects = $objects->filter(array('dept_id__in'=>$options['depts']));
if (!$flat)
return $objects;
$addresses = array();
foreach ($objects->values_flat('email_id', 'email') as $row) {
list($id, $email) = $row;
$addresses[$id] = $email;
}
return $addresses;
}
static function getPermissions() {
return self::$perms;
}
// Supported Remote Mailbox protocols
static function mailboxProtocols() {
return [
'IMAP' => 'IMAP',
'POP' => 'POP'];
}
}
RolePermission::register(/* @trans */ 'Miscellaneous', Email::getPermissions());
class EmailAccount extends VerySimpleModel {
static $meta = array(
'table' => EMAIL_ACCOUNT_TABLE,
'pk' => array('id'),
'joins' => array(
'email' => array(
'constraint' => array('email_id' => 'Email.email_id'),
),
),
);
private $bkId;
private $form;
private $cred;
private $config;
private $instance;
// If account supports tls or ssl
private $encryption = false;
// Account settings
private $settings;
public function getAccountSetting($stashed=false) {
if (!isset($this->settings)) {
// Set properties to stashed form data (if any and requested)
if ($stashed && ($info=$this->getInfo())) {
foreach (['host', 'port', 'protocol'] as $p) {
$k = "{$this->type}_$p";
if (isset($info[$k]))
$this->{$p} = $info[$k];
}
}
$this->settings = new osTicket\Mail\AccountSetting($this);
}
return $this->settings;
}
public function getHostInfo() {
return $this->getAccountSetting()->getHostInfo();
}
public function getHost() {
return $this->host;
}
public function getPort() {
return $this->port;
}
public function getProtocol() {
return $this->protocol;
}
public function getNumErrors() {
return $this->num_errors;
}
public function isOAuthAuth() {
return str_starts_with($this->getAuthBk(), 'oauth');
}
public function isBasicAuth() {
return str_starts_with($this->getAuthBk(), 'basic');
}
public function isActive() {
return ($this->active && $this->hasCredentials());
}
// **** Don't use it ****
// This routine is depricated and will be removed in the future - OAuth2
// Plugin uses it to check if the email accout has auth2 backend.
public function isEnabled() {
return $this->isOAuthAuth();
}
public function isAuthBackendEnabled() {
return $this->isOAuthAuth()
? (($i=$this->getOAuth2Instance()) && $i->isEnabled())
: true;
}
public function isStrict() {
return $this->getConfig()->getStrictMatching();
}
public function checkStrictMatching($token=null) {
$token ??= $this->getAccessToken();
return ($token && $token->isMatch(
$this->getEmail()->getEmail(),
$this->isStrict()));
}
public function shouldAuthorize() {
// check status and make sure it's oauth
if (!$this->isAuthBackendEnabled() || !$this->isOAuthAuth())
return false;
return (!($cred=$this->getFreshCredentials())
// Get token with signature match - mismatch means config
// changed somehow
|| !($token=$cred->getAccessToken($this->getConfigSignature()))
// Check if expired
|| $token->isExpired()
// If Strict Matching is enabled ensure the email matches
// the Resource Owner
|| !$this->checkStrictMatching($token));
}
public function getId() {
return $this->id;
}
public function getType() {
return $this->type;
}
public function getAuthBk() {
return $this->auth_bk;
}
public function getAuthId() {
return $this->auth_id;
}
public function getBkId() {
if (!isset($this->bkId)) {
$id = sprintf('%s:%d',
$this->getAuthBk(), $this->getId());
if ($this->isOAuthAuth())
$id .= sprintf(':%d:%b',
$this->getAuthId(), $this->isStrict()); #TODO: Remove strict and delegate to email account
$this->bkId = $id;
}
return $this->bkId;
}
public function getEmailId() {
return $this->email_id;
}
public function getEmail() {
return $this->email;
}
public function getName() {
return $this->getEmail()->getName();
}
public function getAccessToken() {
$cred = $this->getFreshCredentials();
return $cred ? $cred->getAccessToken($this->getConfigSignature()) : null;
}
private function getOAuth2Backend($auth=null) {
$auth = $auth ?: $this->getAuthBk();
return Oauth2AuthorizationBackend::getBackend($auth);
}
public function getOAuth2ConfigDefaults() {
$email = $this->getEmail();
return [
'auth_type' => 'autho',
'auth_name' => $email->getName(),
'name' => sprintf('%s (%s)',
$email->getEmail(), $this->getType()),
'isactive' => 1,
'strict_matching' => $this->isStrict(),
'notes' => sprintf(
__('OAuth2 Authorization for %s'), $email->getEmail()),
];
}
public function getOAuth2ConfigInfo() {
$vars = $this->getOAuth2ConfigDefaults();
if (($i=$this->getOAuth2Instance()))
$vars = array_merge($vars, $i->getInfo());
return $vars;
}
private function getOAuth2ConfigForm($vars, $auth=null) {
// Lookup OAuth2 backend & Get basic config form
if (($bk=$this->getOAuth2Backend($auth)))
return $bk->getConfigForm(
array_merge(
$this->getOAuth2ConfigDefaults(),
$vars ?: $bk->getDefaults() #nolint
),
!strcmp($auth, $this->getAuthBk())
? $this->getAuthId() : null
);
}
private function getBasicAuthConfigForm($vars, $auth=null) {
$creds = $this->getCredentialsVars($auth) ?: [];
if (!$vars && $creds) {
$vars = [
'username' => $creds['username'],
'passwd' => $creds['password'],
];
} elseif (!$_POST && !isset($vars['username']) && $this->email)
$vars['username'] = $this->email->getEmail();
if (!isset($vars['passwd']) && $_POST && $creds)
$vars['passwd'] = $creds['password'];
return new BasicAuthConfigForm($vars);
}
public function getOAuth2Instance($bk=null) {
$bk = $bk ?: $this->getOAuth2Backend();
if (!isset($this->instance) && $this->getAuthId() && $bk)
$this->instance = $bk->getPluginInstance($this->getAuthId());
return $this->instance;
}
public function getConfigSignature() {
if (($i=$this->getOAuth2Instance()))
return $i->getSignature();
}
public function getInfo() {
$ht = array();
foreach (static::$vars as $var) {
if (isset($this->ht[$var]))
$ht[$this->type.'_'.$var] = $this->ht[$var];
}
// Add stashed info (if any)
if (($data=$this->email->restoreFormData(false)))
$ht = array_merge($ht, $data);
return $ht;
}
private function getNamespace() {
return sprintf('email.%d.account.%d',
$this->getEmailId(),
$this->getId());
}
protected function getConfig() {
if (!isset($this->config))
$this->config = new EmailAccountConfig($this->getNamespace());
return $this->config;
}
public function getAuthConfigForm($auth, $vars=false) {
if (!isset($this->form) || strcmp($auth, $this->getAuthBk())) {
list($type, $provider) = explode(':', $auth);
switch ($type) {
case 'oauth2':
$this->form = $this->getOAuth2ConfigForm($vars, $auth);
break;
case 'basic':
$this->form = $this->getBasicAuthConfigForm($vars,
$auth);
$setting = $this->getAccountSetting(true);
if (!$setting || !$setting->isValid())
$this->form->setNotice(
__('Host, Port & Protocol Required'));
break;
}
}
return $this->form;
}
public function saveAuth($auth, $form, &$errors) {
// Validate the form
if (!$form->isValid())
return false;
$vars = $form->getClean();
list($type, $provider) = explode(':', $auth);
switch ($type) {
case 'basic':
// Set username and password
if (!$this->updateCredentials($auth, $vars, $errors)
&& !isset($errors['err']))
$errors['err'] = sprintf('%s %s',
__('Error Saving'),
__('Authentication'));
break;
case 'oauth2':
// For OAuth we are simply saving configuration -
// credetials are saved post successful authorization
// redirect.
// Lookup OAuth backend
if (($bk=$this->getOAuth2Backend($auth))) {
// Merge form data, post vars and any defaults
$vars = array_merge($this->getOAuth2ConfigDefaults(),
array_intersect_key($_POST, $this->getOAuth2ConfigDefaults()),
$vars);
// Update or add OAuth2 instance
if ($this->getAuthId()
&& ($i=$bk->getPluginInstance($this->getAuthId()))) {
$vars = array_merge($bk->getDefaults(), $vars); #nolint
if ($i->update($vars, $errors)) {
// Disable account if backend is changed
if (strcasecmp($this->auth_bk, $auth))
$this->active = 0;
// Auth backend can be changed on update
$this->auth_bk = $auth;
$this->save();
// Update Strict Matching
$this->getConfig()->setStrictMatching($_POST['strict_matching'] ? 1 : 0);
} elseif (!isset($errors['err'])) {
$errors['err'] = sprintf('%s %s',
__('Error Saving'),
__('Authentication'));
}
} else {
// Ask the backend to add OAuth2 instance for this account
if (($i=$bk->addPluginInstance($vars, $errors))) { #nolint
// Cache instance
$this->instance = $i;
$this->auth_bk = $auth;
$this->auth_id = $i->getId();
$this->save();
} else {
$errors['err'] = __('Error Adding OAuth2 Instance');
}
}
}
break;
default:
$errors['err'] = __('Unknown Authentication Type');
}
return !($errors);
}
public function logError($error) {
return $this->logActivity($error);
}
public function hasCredentials() {
return ($this->getFreshCredentials());
}
private function getCredentialsVars($auth=null) {
$vars = [];
if (($cred = $this->getCredentials($auth)))
$vars = $cred->toArray();
return $vars;
}
public function getFreshCredentials($auth=null) {
return $this->getCredentials($auth, true);
}
public function getCredentials($auth=null, $refresh=false) {
// Authentication doesn't match - it's getting reconfigured.
if ($auth
&& strncasecmp($this->getAuthBk(), $auth, strlen($auth))
&& !in_array($auth, ['none', 'mailbox']))
return [];
if (!isset($this->cred) || $refresh) {
$this->cred = $cred = null;
$auth = $auth ?: $this->getAuthBk();
list($type, $provider) = explode(':', $auth);
try {
switch ($type) {
case 'mailbox':
$cred = $this->getMailBoxCredentials($refresh);
break;
case 'none':
// No authentication required (open replay)
$cred = new osTicket\Mail\NoAuthCredentials([
'username' => $this->email->getEmail()]);
break;
case 'basic':
$cred = $this->getBasicAuthCredentials();
break;
case 'oauth2':
$cred = $this->getOAuth2AuthCredentials($provider, $refresh);
break;
default:
throw new Exception(sprintf('%s: %s',
$type, __('Unknown Credential Type')));
}
// Cache the credentials
$this->cred = $cred;
} catch (Exception $ex) {
// Log the error
$this->logError(sprintf('%s: %s',
__('Credentials'), $ex->getMessage()
));
}
}
return $this->cred;
}
private function getMailBoxCredentials($refresh=false) {
if (($mb=$this->email->getMailBoxAccount())
&& $mb->getAuthBk())
return $mb->getCredentials($mb->getAuthBk(), $refresh);
}
private function getBasicAuthCredentials() {
if (($c=$this->getConfig())
&& ($creds=$c->toArray())
&& isset($creds['username'])
&& isset($creds['passwd'])) {
return new osTicket\Mail\BasicAuthCredentials([
'username' => $creds['username'],
// Decrypt password
'password' => Crypto::decrypt($creds['passwd'],
SECRET_SALT,
md5($creds['username'].$this->getNamespace()))
]);
}
}
private function updateBasicAuthCredentials($vars, &$errors) {
// Get current credentials - we need to re-encrypt
// password as username might be changing
$creds = $this->getCredentialsVars('basic');
// password change?
if (!$vars['username']) {
$errors['username'] = __('Username Required');
} elseif (!$vars['passwd'] && !$creds['password']) {
$errors['passwd'] = __('Password Required');
} elseif (($setting=$this->getAccountSetting(true))
&& !$setting->isValid()) {
$errors['err'] = implode(', ', $setting->getErrors());
} elseif ($setting && !$errors) {
// Validate the credentials
try {
$cred = new osTicket\Mail\BasicAuthCredentials([
'username' => $vars['username'],
'password' => $vars['passwd'] ?:
$creds['password'],
]);
if (!$this->validateCredentials($cred))
$errors['err'] = __('Invalid Credentials');
} catch (Exception $ex) {
$errors['err'] = $ex->getMessage();
}
}
if (!$errors) {
// Save credentials and get out of here.
$info = [
// username
'username' => $vars['username'],
// Encrypt password
'passwd' => Crypto::encrypt($vars['passwd'] ?:
$creds['password'], SECRET_SALT,
md5($vars['username'].$this->getNamespace()))
];
if (!$this->getConfig()->updateInfo($info))
$errors['err'] = sprintf('%s: %s',
__('BasicAuth'),
__('Error saving credentials'));
}
return !count($errors);
}
private function getOAuth2AuthCredentials($provider, $refresh=false) {
if (!($c=$this->getConfig()))
return false;
$creds=$c->toArray();
// Decrypt Access Token
if ($creds['access_token']) {
$creds['access_token'] = Crypto::decrypt(
$creds['access_token'],
SECRET_SALT,
md5($creds['resource_owner_email'].$this->getNamespace())
);
}
// Decrypt Referesh Token
if ($creds['refresh_token']) {
$creds['refresh_token'] = Crypto::decrypt(
$creds['refresh_token'],
SECRET_SALT,
md5($creds['resource_owner_email'].$this->getNamespace())
);
}
try {
// Init credentials and see of we need to
// refresh the token
$errors = [];
$auth = sprintf('oauth2:%s', $provider);
$class = 'osTicket\Mail\OAuth2AuthCredentials';
if (($cred=$class::init($creds))
&& ($token=$cred->getToken())
&& ($refresh && $token->isExpired())
&& ($bk=$this->getOAuth2Backend())
&& ($info=$bk->refreshAccessToken( #nolint
$token->getRefreshToken(),
$this->getBkId(), $errors))
&& isset($info['access_token'])
&& $this->updateCredentials($auth,
// Merge new access token with
// already decrypted creds
array_merge($creds, $info), $errors
)) {
return $this->getCredentials($auth, $refresh);
} elseif ($errors) {
// Throw an exception with the error
throw new Exception($errors['refresh_token']
?? __('Referesh Token Expired'));
}
} catch (Exception $ex) {
// rethrow the exception including above.
throw $ex;
}
return $cred;
}
private function updateOAuth2AuthCredentials($provider, $vars, &$errors) {
$err = sprintf('%s_auth_bk', $this->getType());
if (!$vars['access_token']) {
$errors[$err] = __('Access Token Required');
} elseif (!$vars['resource_owner_email']
|| !Validator::is_email($vars['resource_owner_email'])) {
$errors[$err] = __('Resource Owner Required');
} elseif ($this->isStrict()
// When in Strict mode Account Email must match resource owner's
// email. Strict mode can be disabled for a global admin to
// authorized onbehalf of other user accounts or shared mailboxes.
&& strcasecmp($this->getEmail()->getEmail(), $vars['resource_owner_email'])) {
$errors[$err] = sprintf(__('Strict Mode: Expecting Authorization for %s not %s'),
$this->getEmail()->getEmail(),
$vars['resource_owner_email']);
} elseif (!$errors) {
// Encrypt Access Token
$vars['access_token'] = Crypto::encrypt(
$vars['access_token'],
SECRET_SALT,
md5($vars['resource_owner_email'].$this->getNamespace()));
// Encrypt Referesh Token
if ($vars['refresh_token']) {
$vars['refresh_token'] = Crypto::encrypt(
$vars['refresh_token'],
SECRET_SALT,
md5($vars['resource_owner_email'].$this->getNamespace())
);
}
$vars['config_signature'] = $this->getConfigSignature();
// TODO: Validate
if (!$this->getConfig()->updateInfo($vars))
$errors['err'] = sprintf('oauth2:%s - %s',
Format::htmlchars($provider),
__('Error saving credentials'));
}
return !count($errors);
}
public function updateCredentials($auth, $vars, &$errors) {
if (!$vars || $errors)
return false;
list($type, $provider) = explode(':', $auth);
switch ($type) {
case 'basic':
if (!($this->updateBasicAuthCredentials($vars, $errors)))
return false;
break;
case 'oauth2':
if (!($this->updateOAuth2AuthCredentials($provider, $vars, $errors)))
return false;
break;
default:
$errors['err'] = sprintf('%s - %s',
__('Unknown Authentication'),
Format::htmlchars($auth));
}
if ($errors)
return false;
// Save the auth backend
$this->auth_bk = $auth;
// Clear cached credentials
$this->creds = null;
return $this->save();
}
/*
* Destory the account config
*/
function destroyConfig() {
return $this->getConfig()->destroy();
}
function update($vars, &$errors) {
return false;
}
public function logActivity($error=null, $now=null) {
if (isset($error)) {
$this->num_errors += 1;
$this->last_error_msg = $error;
$this->last_error = $now ?: SqlFunction::NOW();
} else {
$this->num_errors = 0;
$this->last_error = null;
$this->last_error_msg = null;
$this->last_activity = $now ?: SqlFunction::NOW();
}
return $this->save();
}
function save($refetch=false) {
if ($this->dirty) {
$this->updated = SqlFunction::NOW();
}
return parent::save($refetch || $this->dirty);
}
function delete() {
// Destroy the Email config
$this->destroyConfig();
// Delete the Plugin instance
if ($this->isOAuthAuth() && ($i=$this->getOAuth2Instance()))
$i->delete();
// Delete the EmailAccount
parent::delete();
}
static function create($ht=false) {
$i = new static($ht);
$i->active = isset($ht['active']) ? $ht['active'] : 0;
$i->created = SqlFunction::NOW();
return $i;
}
}
class MailBoxAccount extends EmailAccount {
static $meta = array(
'table' => EMAIL_ACCOUNT_TABLE,
'pk' => array('id'),
'joins' => array(
'email' => array(
'constraint' => array('email_id' => 'Email.email_id'),
),
'account' => array(
'constraint' => array(
'type' => "'mailbox'",
'email_id' => 'Email.email_id'),
),
),
);
static protected $vars = [
'active', 'host', 'port', 'protocol', 'auth_bk', 'folder',
'fetchfreq', 'fetchmax', 'postfetch', 'archivefolder'
];
private $cred;
private $mailbox;
static public function objects() {
return parent::objects()
->filter(['type' => 'mailbox']);
}
//TODO: Morhp MailBoxAccount to ImapMailBoxAccount
public function isImap() {
return (strcasecmp($this->getProtocol(), 'IMAP') === 0);
}
//TODO: Morhp MailBoxAccount to PopMailBoxAccount
public function isPop() {
return (strcasecmp($this->getProtocol(), 'POP') === 0);
}
public function getProtocol() {
return $this->protocol;
}
public function getFolder() {
return $this->folder;
}
public function getFetchFolder() {
return $this->getFolder();
}
public function getArchiveFolder() {
return ($this->isImap() && $this->archivefolder)
? $this->archivefolder : null;
}
public function canDeleteEmails() {
return !strcasecmp($this->postfetch, 'delete');
}
public function getMaxFetch() {
return $this->fetchmax;
}
protected function validateCredentials(osTicket\Mail\AuthCredentials $creds) {
return $this->getMailBox($creds);
}
public function getMailBox(osTicket\Mail\AuthCredentials $cred=null) {
if (!isset($this->mailbox) || $cred) {
$this->cred = $cred ?: $this->getFreshCredentials();
$setting = $this->getAccountSetting();
$setting->setCredentials($this->cred);
switch (strtolower($this->getProtocol())) {
case 'imap':
$mailbox = new osTicket\Mail\Imap($setting);
break;
case 'pop3':
case 'pop':
$mailbox = new osTicket\Mail\Pop3($setting);
break;
default:
throw new Exception('Unknown Mail protocol:
'.$this->getProtocol());
}
$this->mailbox = $mailbox;
}
return $this->mailbox;
}
public function fetchEmails() {
try {
$this->logLastFetch();
$fetcher = new osTicket\Mail\Fetcher($this);
return $fetcher->processEmails();
} catch (Throwable $t) {
// May throw an Exception or Error
// Log the message
$this->logFetchError($t->getMessage());
// rethrow the throwable so caller can handle it
throw $t;
}
return 0;
}
protected function setInfo($vars, &$errors) {
$creds = null;
if ($vars['mailbox_active']) {
if (!$vars['mailbox_host'])
$errors['mailbox_host'] = __('Host name required');
if (!$vars['mailbox_port'])
$errors['mailbox_port'] = __('Port required');
if (!$vars['mailbox_protocol'])
$errors['mailbox_protocol'] = __('Select protocol');
elseif (!in_array($vars['mailbox_protocol'], Email::mailboxProtocols()))
$errors['mailbox_protocol'] = __('Invalid protocol');
if (!$vars['mailbox_auth_bk'])
$errors['mailbox_auth_bk'] = __('Select Authentication');
if (!$vars['mailbox_fetchfreq'] || !is_numeric($vars['mailbox_fetchfreq']))
$errors['mailbox_fetchfreq'] = __('Fetch interval required');
if (!$vars['mailbox_fetchmax'] || !is_numeric($vars['mailbox_fetchmax']))
$errors['mailbox_fetchmax'] = __('Maximum emails required');
if ($vars['mailbox_protocol'] == 'POP' && !empty($vars['mailbox_folder']))
$errors['mailbox_folder'] = __('POP mail servers do not support folders');
if (!$vars['mailbox_postfetch'])
$errors['mailbox_postfetch'] = __('Indicate what to do with fetched emails');
}
if (!strcasecmp($vars['mailbox_postfetch'], 'archive')) {
if ($vars['mailbox_protocol'] == 'POP')
$errors['mailbox_postfetch'] = __('POP mail servers do not support folders');
elseif (!$vars['mailbox_archivefolder'])
$errors['mailbox_postfetch'] = __('Valid folder required');
elseif (!strcasecmp($vars['mailbox_folder'],
$vars['mailbox_archivefolder']))
$errors['mailbox_postfetch'] = __('Archive folder cannot be same as fetched folder (INBOX)');
}
// Make sure authentication is configured if selection is made
if ($vars['mailbox_auth_bk']
&& !($creds=$this->getFreshCredentials($vars['mailbox_auth_bk'])))
$errors['mailbox_auth_bk'] = __('Configure Authentication');
if (!$errors) {
$this->active = $vars['mailbox_active'] ? 1 : 0;
$this->host = $vars['mailbox_host'];
$this->port = $vars['mailbox_port'] ?: 0;
$this->protocol = $vars['mailbox_protocol'];
$this->auth_bk = $vars['mailbox_auth_bk'] ?: null;
$this->folder = $vars['mailbox_folder'] ?: null;
$this->fetchfreq = $vars['mailbox_fetchfreq'] ?: 5;
$this->fetchmax = $vars['mailbox_fetchmax'] ?: 30;
$this->postfetch = $vars['mailbox_postfetch'];
$this->last_activity = null;
$this->last_error_msg = null;
$this->num_errors = 0;
//Post fetch email handling...
switch ($vars['mailbox_postfetch']) {
case 'archive':
$this->archivefolder = $vars['mailbox_archivefolder'];
break;
case 'delete':
default:
$this->archivefolder = null;
}
// If mailbox is active and we have credentials then attemp to
// authenticate
if ($this->active && $creds) {
try {
// Get mailbox (Storage Backend)
if (($mb=$this->getMailBox($creds))) {
if ($this->folder &&
!$mb->hasFolder($this->folder))
$errors['mailbox_folder'] = __('Unknown Folder');
if ($this->archivefolder
&& $this->isImap()
&& !$mb->hasFolder($this->archivefolder)
&& !$mb->createFolder($this->archivefolder))
$errors['mailbox_archivefolder'] =
__('Unable to create Folder');
} else {
$errors['mailbox_auth'] = __('Authentication Error');
}
} catch (Exception $ex) {
$errors['mailbox_auth'] = $ex->getMessage();
}
}
}
return !$errors;
}
public function logLastFetch($now=null) {
return $this->logActivity(null, $now);
}
private function logFetchError($error) {
return $this->logActivity($error ?: __('Mail Fetch Error'));
}
public function update($vars, &$errors) {
if (!$this->setInfo($vars, $errors))
return false;
return $this->save();
}
static function create($ht=false) {
$i = parent::create($ht);
$i->type = 'mailbox';
return $i;
}
}
class SmtpAccount extends EmailAccount {
static $meta = array(
'table' => EMAIL_ACCOUNT_TABLE,
'pk' => array('id'),
'joins' => array(
'email' => array(
'constraint' => array('email_id' => 'Email.email_id'),
),
'account' => array(
'constraint' => array(
'type' => "'smtp'",
'email_id' => 'Email.email_id'),
),
),
);
static protected $vars = [
'active', 'host', 'port', 'protocol', 'auth_bk', 'allow_spoofing'
];
private $smtp;
private $cred;
static public function objects() {
return parent::objects()
->filter(['type' => 'smtp']);
}
public function isMailboxAuth() {
return (strcasecmp($this->getAuthBk(), 'mailbox') === 0);
}
/*
* Check if using mailbox auth and MailboxAccount exists if so
* return the MailboxAccount config, otherwise return it's own
* config
*/
protected function getConfig() {
if ($this->isMailboxAuth()
&& ($email=$this->getEmail())
&& ($account=$email->getMailBoxAccount()))
return $account->getConfig();
return parent::getConfig();
}
public function allowSpoofing() {
return ($this->allow_spoofing);
}
protected function validateCredentials(osTicket\Mail\AuthCredentials $creds) {
return $this->getSmtp($creds);
}
public function getSmtpConnection() {
$this->smtp = $this->getSmtp();
if (!$this->smtp->connect())
return false;
return $this->smtp;
}
public function getSmtp(osTicket\Mail\AuthCredentials $cred=null) {
if (!isset($this->smtp) || $cred) {
$this->cred = $cred ?: $this->getFreshCredentials();
if ($this->cred) {
$setting = $this->getAccountSetting();
$setting->setCredentials($this->cred);
$smtpOptions = new osTicket\Mail\SmtpOptions($setting);
$smtp = new osTicket\Mail\Smtp($smtpOptions);
// Attempt to connect now if credentials are sent in
if ($cred) $smtp->connect();
$this->smtp = $smtp;
}
}
return $this->smtp;
}
protected function setInfo($vars, &$errors) {
$creds = null;
$_errors = [];
if ($vars['smtp_active']) {
if (!$vars['smtp_host'])
$_errors['smtp_host'] = __('Host name required');
if (!$vars['smtp_port'])
$_errors['smtp_port'] = __('Port required');
if (!$vars['smtp_auth_bk'])
$_errors['smtp_auth_bk'] = __('Select Authentication');
elseif (!($creds=$this->getFreshCredentials($vars['smtp_auth_bk'])))
$_errors['smtp_auth_bk'] = ($vars['smtp_auth_bk'] == 'mailbox')
? __('Configure Mailbox Authentication')
: __('Configure Authentication');
} elseif ($vars['smtp_auth_bk']
// We default to mailbox - so we're not going to check
// unless account is active, see above!
&& strcasecmp($vars['smtp_auth_bk'], 'mailbox')
&& !($creds=$this->getFreshCredentials($vars['smtp_auth_bk'])))
$_errors['smtp_auth_bk'] = __('Configure Authentication');
// Check if set to active and using mailbox auth, if so check strict
// matching.
if ($vars['smtp_active'] == 1
&& ($vars['smtp_auth_bk'] === 'mailbox')
&& (strpos($vars['mailbox_auth_bk'], 'oauth2') === 0)
&& !$this->checkStrictMatching())
$_errors['smtp_auth_bk'] = sprintf('%s and %s', __('Resource Owner'), __('Email Mismatch'));
if (!$_errors) {
$this->active = $vars['smtp_active'] ? 1 : 0;
$this->host = $vars['smtp_host'];
$this->port = $vars['smtp_port'] ?: 0;
$this->auth_bk = $vars['smtp_auth_bk'] ?: null;
$this->protocol = 'SMTP';
$this->allow_spoofing = $vars['smtp_allow_spoofing'] ? 1 : 0;
$this->last_activity = null;
$this->last_error_msg = null;
$this->num_errors = 0;
// If account is active then attempt to authenticate
if ($this->active && $creds) {
try {
if (!($smtp=$this->getSmtp($creds)))
$_errors['smtp_auth'] = __('Authentication Error');
} catch (Exception $ex) {
$_errors['smtp_auth'] = $ex->getMessage();
}
}
}
$errors = array_merge($errors, $_errors);
return !$errors;
}
function update($vars, &$errors) {
if (!$this->setInfo($vars, $errors))
return false;
return $this->save();
}
static function create($ht=false) {
$i = parent::create($ht);
$i->type = 'smtp';
return $i;
}
}
/*
* Email Config Store
*
* Extends base central config store
*
*/
class EmailAccountConfig extends Config {
/*
* Get strict matching (default: true)
*/
public function getStrictMatching() {
return $this->get('strict_matching', true);
}
/*
* Set strict matching
*/
public function setStrictMatching($mode) {
return $this->set('strict_matching', !!$mode);
}
public function updateInfo($vars) {
return parent::updateAll($vars);
}
}
/*
* Basic Authentication Configuration Form
*
*/
class BasicAuthConfigForm extends AbstractForm {
private $account;
private $host;
private $password;
function __construct($source=null, $options=array()) {
parent::__construct($source, $options);
}
private function getPassword() {
return $this->_source['passwd'];
}
function buildFields() {
$password = $this->getPassword();
return array(
'username' => new TextboxField(array(
'required' => true,
'label' => __('Username'),
'configuration' => array(
'length' => 0,
'autofocus' => true,
),
)),
'passwd' => new PasswordField(array(
'label' => __('Password'),
'required' => !$password,
'validator' => 'noop',
'hint' => $password
? __('Enter a new password to change current one')
: '',
'configuration' => array(
'length' => 0,
'classes' => 'span12',
'placeholder' => $password
? str_repeat('•', strlen($password)*2)
: __('Password'),
),
)),
);
}
}
?>