Bigchange for contact image avatar support #115
|
@ -22,10 +22,12 @@
|
||||||
view.viewUserPicVisible = ko.observable(false);
|
view.viewUserPicVisible = ko.observable(false);
|
||||||
|
|
||||||
view.message.subscribe(msg => {
|
view.message.subscribe(msg => {
|
||||||
|
view.viewUserPicVisible(false);
|
||||||
if (msg) {
|
if (msg) {
|
||||||
let from = msg.from[0],
|
let from = msg.from[0],
|
||||||
bimi = 'pass' == from.dkimStatus ? 1 : 0;
|
bimi = 'pass' == from.dkimStatus ? 1 : 0;
|
||||||
// view.viewUserPic(`?Avatar/${bimi}/${encodeURIComponent(from.email)}`);
|
// view.viewUserPic(`?Avatar/${bimi}/${encodeURIComponent(from.email)}`);
|
||||||
|
// view.viewUserPicVisible(true);
|
||||||
rl.pluginRemoteRequest((iError, data) => {
|
rl.pluginRemoteRequest((iError, data) => {
|
||||||
if (!iError && data?.Result.type) {
|
if (!iError && data?.Result.type) {
|
||||||
view.viewUserPic(`data:${data.Result.type};base64,${data.Result.data}`);
|
view.viewUserPic(`data:${data.Result.type};base64,${data.Result.data}`);
|
||||||
|
@ -38,6 +40,23 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
|
if ('MailMessageList' === e.detail.viewModelTemplateID) {
|
||||||
|
const
|
||||||
|
template = document.getElementById('MailMessageList' ),
|
||||||
|
messageCheckbox = template.content.querySelector('.messageCheckbox');
|
||||||
|
messageCheckbox.dataset.bind = 'attr:{style:$root.viewUserPic($data)}';
|
||||||
|
e.detail.viewUserPic = msg => {
|
||||||
|
let from = msg.from[0],
|
||||||
|
bimi = 'pass' == from.dkimStatus ? 1 : 0;
|
||||||
|
return `background:no-repeat url("?Avatar/${bimi}/${encodeURIComponent(from.email)}") center / contain`;
|
||||||
|
return `background:no-repeat url("?Avatar/${bimi}/${encodeURIComponent(from.email)}") right / 32px;width:68px`;
|
||||||
|
};
|
||||||
|
.checkboxMessage {
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
});
|
});
|
||||||
|
|
||||||
})(window.rl);
|
})(window.rl);
|
||||||
|
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 6.5 KiB |
Before Width: | Height: | Size: 995 B |
Before Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 6 KiB |
Before Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 9.9 KiB |
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 382 B |
Before Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 7.4 KiB |
Before Width: | Height: | Size: 7.4 KiB |
Before Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 4.5 KiB |
|
@ -8,7 +8,7 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
URL = 'https://snappymail.eu/',
|
URL = 'https://snappymail.eu/',
|
||||||
VERSION = '1.0',
|
VERSION = '1.0',
|
||||||
RELEASE = '2022-11-11',
|
RELEASE = '2022-11-11',
|
||||||
REQUIRED = '2.21.0',
|
REQUIRED = '2.22.0',
|
||||||
CATEGORY = 'Contacts',
|
CATEGORY = 'Contacts',
|
||||||
LICENSE = 'MIT',
|
LICENSE = 'MIT',
|
||||||
DESCRIPTION = '';
|
DESCRIPTION = '';
|
||||||
|
@ -53,48 +53,82 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// $this->verifyCacheByKey($sEmail);
|
$oActions = \RainLoop\Api::Actions();
|
||||||
|
$oActions->verifyCacheByKey($sEmail);
|
||||||
|
|
||||||
// DATA_IMAGE_USER_DOT_PIC
|
$aResult = null;
|
||||||
$sDomain = \explode('@', $sEmail);
|
|
||||||
$sDomain = \array_pop($sDomain);
|
|
||||||
|
|
||||||
$BIMI = $bBimi ? \SnappyMail\DNS::BIMI($sDomain) : null;
|
|
||||||
// TODO: process $BIMI value
|
|
||||||
|
|
||||||
// TODO: lookup contacts vCard
|
// TODO: lookup contacts vCard
|
||||||
|
$oAccount = $oActions->getAccountFromToken();
|
||||||
// TODO: make this optional
|
if ($oAccount) {
|
||||||
$aResult = static::Gravatar($sEmail);
|
$oAddressBookProvider = $oActions->AddressBookProvider($oAccount);
|
||||||
|
if ($oAddressBookProvider) {
|
||||||
if (!$aResult && \file_exists(__DIR__ . '/images/services/'.$sDomain.'.png')) {
|
$oContact = $oAddressBookProvider->GetContactByEmail($sEmail);
|
||||||
$aResult = [
|
if ($oContact && $oContact->vCard && $oContact->vCard['PHOTO']) {
|
||||||
'image/png',
|
$aResult = [
|
||||||
\file_get_contents(__DIR__ . '/images/services/'.$sDomain.'.png')
|
'text/vcard',
|
||||||
];
|
$oContact->vCard
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$aResult) {
|
if (!$aResult) {
|
||||||
$aResult = [
|
$sDomain = \explode('@', $sEmail);
|
||||||
'image/png',
|
$sDomain = \array_pop($sDomain);
|
||||||
\file_get_contents(__DIR__.'/images/empty-contact.png')
|
|
||||||
];
|
$aUrls = [];
|
||||||
|
|
||||||
|
$BIMI = $bBimi ? \SnappyMail\DNS::BIMI($sDomain) : null;
|
||||||
|
if ($BIMI) {
|
||||||
|
$aUrls[] = $BIMI;
|
||||||
|
// $aResult = ['text/uri-list', $BIMI];
|
||||||
|
\SnappyMail\Log::debug('Avatar', "BIMI {$sDomain} for {$sUrl}");
|
||||||
|
} else {
|
||||||
|
\SnappyMail\Log::notice('Avatar', "BIMI 404 for {$sDomain}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: make Gravatar optional
|
||||||
|
$sAsciiEmail = \MailSo\Base\Utils::IdnToAscii($sEmail, true);
|
||||||
|
$aUrls[] = 'http://gravatar.com/avatar/'.\md5(\strtolower($sAsciiEmail)).'?s=80&d=404';
|
||||||
|
|
||||||
|
foreach ($aUrls as $sUrl) {
|
||||||
|
if ($aResult = static::getUrl($sUrl)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// $this->cacheByKey($sEmail);
|
if (!$aResult) {
|
||||||
|
$aServices = [
|
||||||
|
"services/{$sDomain}",
|
||||||
|
'services/' . \preg_replace('/^.+\\.([^.]+\\.[^.]+)$/D', '$1', $sDomain),
|
||||||
|
'empty-contact' // DATA_IMAGE_USER_DOT_PIC
|
||||||
|
];
|
||||||
|
foreach ($aServices as $service) {
|
||||||
|
if (\file_exists(__DIR__ . "/images/{$service}.png")) {
|
||||||
|
$aResult = [
|
||||||
|
'image/png',
|
||||||
|
\file_get_contents(__DIR__ . "/images/{$service}.png")
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$oActions->cacheByKey($sEmail);
|
||||||
|
|
||||||
return $aResult;
|
return $aResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function Gravatar(string $sEmail) : ?array
|
private static function getUrl(string $sUrl) : ?array
|
||||||
{
|
{
|
||||||
$sEmail = \MailSo\Base\Utils::IdnToAscii($sEmail, true);
|
|
||||||
$sGravatarUrl = 'http://gravatar.com/avatar/'.\md5(\strtolower($sEmail)).'?s=80&d=404';
|
|
||||||
$oHTTP = \SnappyMail\HTTP\Request::factory(/*'socket' or 'curl'*/);
|
$oHTTP = \SnappyMail\HTTP\Request::factory(/*'socket' or 'curl'*/);
|
||||||
$oHTTP->proxy = \RainLoop\Api::Config()->Get('labs', 'curl_proxy', '');
|
$oHTTP->proxy = \RainLoop\Api::Config()->Get('labs', 'curl_proxy', '');
|
||||||
$oHTTP->proxy_auth = \RainLoop\Api::Config()->Get('labs', 'curl_proxy_auth', '');
|
$oHTTP->proxy_auth = \RainLoop\Api::Config()->Get('labs', 'curl_proxy_auth', '');
|
||||||
$oHTTP->max_response_kb = 0;
|
$oHTTP->max_response_kb = 0;
|
||||||
$oHTTP->timeout = 15; // timeout in seconds.
|
$oHTTP->timeout = 15; // timeout in seconds.
|
||||||
$oResponse = $oHTTP->doRequest('GET', $sGravatarUrl);
|
$oResponse = $oHTTP->doRequest('GET', $sUrl);
|
||||||
if ($oResponse) {
|
if ($oResponse) {
|
||||||
if (200 === $oResponse->status && \str_starts_with($oResponse->getHeader('content-type'), 'image/')) {
|
if (200 === $oResponse->status && \str_starts_with($oResponse->getHeader('content-type'), 'image/')) {
|
||||||
return [
|
return [
|
||||||
|
@ -102,11 +136,10 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
$oResponse->body
|
$oResponse->body
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
\SnappyMail\Log::notice('Gravatar', "error {$oResponse->status} for {$sGravatarUrl}");
|
\SnappyMail\Log::notice('Avatar', "error {$oResponse->status} for {$sUrl}");
|
||||||
} else {
|
} else {
|
||||||
\SnappyMail\Log::warning('Gravatar', "failed for {$sGravatarUrl}");
|
\SnappyMail\Log::warning('Avatar', "failed for {$sUrl}");
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -313,6 +313,12 @@ class KolabAddressBook implements \RainLoop\Providers\AddressBook\AddressBookInt
|
||||||
return $aResult;
|
return $aResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function GetContactByEmail(string $sEmail) : ?Contact;
|
||||||
|
{
|
||||||
|
// TODO
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public function GetContactByID($mID, bool $bIsStrID = false) : ?Contact
|
public function GetContactByID($mID, bool $bIsStrID = false) : ?Contact
|
||||||
{
|
{
|
||||||
if ($bIsStrID) {
|
if ($bIsStrID) {
|
||||||
|
|
|
@ -8,7 +8,7 @@ class KolabPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
RELEASE = '2022-09-06',
|
RELEASE = '2022-09-06',
|
||||||
CATEGORY = 'Contacts',
|
CATEGORY = 'Contacts',
|
||||||
DESCRIPTION = 'Use an Address Book of Kolab.',
|
DESCRIPTION = 'Use an Address Book of Kolab.',
|
||||||
REQUIRED = '2.18.0';
|
REQUIRED = '2.22.0';
|
||||||
|
|
||||||
public function Init() : void
|
public function Init() : void
|
||||||
{
|
{
|
||||||
|
|
|
@ -60,6 +60,11 @@ class AddressBook extends AbstractProvider
|
||||||
) : array();
|
) : array();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function GetContactByEmail(string $sEmail) : ?AddressBook\Classes\Contact
|
||||||
|
{
|
||||||
|
return $this->IsActive() ? $this->oDriver->GetContactByEmail($sEmail) : null;
|
||||||
|
}
|
||||||
|
|
||||||
public function GetContactByID($mID, bool $bIsStrID = false) : ?AddressBook\Classes\Contact
|
public function GetContactByID($mID, bool $bIsStrID = false) : ?AddressBook\Classes\Contact
|
||||||
{
|
{
|
||||||
return $this->IsActive() ? $this->oDriver->GetContactByID($mID, $bIsStrID) : null;
|
return $this->IsActive() ? $this->oDriver->GetContactByID($mID, $bIsStrID) : null;
|
||||||
|
|
|
@ -20,6 +20,8 @@ interface AddressBookInterface
|
||||||
|
|
||||||
public function GetContacts(int $iOffset = 0, int $iLimit = 20, string $sSearch = '', int &$iResultCount = 0) : array;
|
public function GetContacts(int $iOffset = 0, int $iLimit = 20, string $sSearch = '', int &$iResultCount = 0) : array;
|
||||||
|
|
||||||
|
public function GetContactByEmail(string $sEmail) : ?Classes\Contact;
|
||||||
|
|
||||||
public function GetContactByID($mID, bool $bIsStrID = false) : ?Classes\Contact;
|
public function GetContactByID($mID, bool $bIsStrID = false) : ?Classes\Contact;
|
||||||
|
|
||||||
public function GetSuggestions(string $sSearch, int $iLimit = 20) : array;
|
public function GetSuggestions(string $sSearch, int $iLimit = 20) : array;
|
||||||
|
|
|
@ -694,6 +694,40 @@ class PdoAddressBook
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $mID
|
||||||
|
*/
|
||||||
|
public function GetContactByEmail(string $sEmail) : ?Contact
|
||||||
|
{
|
||||||
|
$sLowerSearch = $this->specialConvertSearchValueLower($sEmail);
|
||||||
|
|
||||||
|
$sSql = 'SELECT
|
||||||
|
DISTINCT id_contact
|
||||||
|
FROM rainloop_ab_properties
|
||||||
|
WHERE id_user = :id_user
|
||||||
|
AND prop_type = '.PropertyType::JCARD.'
|
||||||
|
AND ('.
|
||||||
|
'prop_value LIKE :search ESCAPE \'=\''
|
||||||
|
. (\strlen($sLowerSearch) ? ' OR (prop_value_lower <> \'\' AND prop_value_lower LIKE :search_lower ESCAPE \'=\')' : '').
|
||||||
|
')';
|
||||||
|
$aParams = array(
|
||||||
|
':id_user' => array($this->iUserID, \PDO::PARAM_INT),
|
||||||
|
':search' => array($this->specialConvertSearchValue($sEmail, '='), \PDO::PARAM_STR)
|
||||||
|
);
|
||||||
|
if (\strlen($sLowerSearch)) {
|
||||||
|
$aParams[':search_lower'] = array($sLowerSearch, \PDO::PARAM_STR);
|
||||||
|
}
|
||||||
|
|
||||||
|
$oContact = null;
|
||||||
|
$iIdContact = 0;
|
||||||
|
|
||||||
|
$aContacts = $this->getContactsFromPDO(
|
||||||
|
$this->prepareAndExecute($sSql, $aParams)
|
||||||
|
);
|
||||||
|
|
||||||
|
return $aContacts ? $aContacts[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param mixed $mID
|
* @param mixed $mID
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -4,6 +4,10 @@ namespace SnappyMail;
|
||||||
|
|
||||||
abstract class DNS
|
abstract class DNS
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* $domain = 'bimigroup.org'
|
||||||
|
* Then a TXT lookup is done on 'default._bimi.bimigroup.org'
|
||||||
|
*/
|
||||||
public static function BIMI(string $domain) : string
|
public static function BIMI(string $domain) : string
|
||||||
{
|
{
|
||||||
$oCache = \RainLoop\Api::Actions()->Cacher();
|
$oCache = \RainLoop\Api::Actions()->Cacher();
|
||||||
|
@ -18,7 +22,7 @@ abstract class DNS
|
||||||
}
|
}
|
||||||
if (null === $BIMI) {
|
if (null === $BIMI) {
|
||||||
$BIMI = '';
|
$BIMI = '';
|
||||||
$values = \dns_get_record($domain, \DNS_TXT);
|
$values = \dns_get_record("default._bimi.{$domain}", \DNS_TXT);
|
||||||
if ($values) {
|
if ($values) {
|
||||||
foreach ($values as $value) {
|
foreach ($values as $value) {
|
||||||
if (\str_starts_with($value['txt'], 'v=BIMI1')) {
|
if (\str_starts_with($value['txt'], 'v=BIMI1')) {
|
||||||
|
|