Bigchange for contact image avatar support #115

This commit is contained in:
the-djmaze 2022-11-22 23:54:32 +01:00
parent 113a421d0f
commit 5069bc2950
32 changed files with 134 additions and 31 deletions

View file

@ -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);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 995 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 382 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

View file

@ -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;
} }
} }

View file

@ -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) {

View file

@ -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
{ {

View file

@ -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;

View file

@ -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;

View file

@ -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
*/ */

View file

@ -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')) {