Skip to content

Commit

Permalink
Add BIMI plugin (roundcube#8143)
Browse files Browse the repository at this point in the history
Brand Indicator Message Identification (BIMI) is an industry-wide standards effort to use brand logos as indicators to help email recipients recognize and avoid fraudulent messages.

See: https://datatracker.ietf.org/doc/draft-brand-indicators-for-message-identification/

Signed-off-by: Craig Andrews <[email protected]>
  • Loading branch information
candrews committed Jan 17, 2023
1 parent 6251d38 commit 4c1207c
Show file tree
Hide file tree
Showing 4 changed files with 237 additions and 0 deletions.
23 changes: 23 additions & 0 deletions plugins/bimi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
Roundcube Webmail BIMI Plugin
=============================
This plugin will use [Brand Indicators for Message Identification (BIMI)](https://bimigroup.org/) icons contact icons if a contact doesn't otherwise have an icon.

Brand Indicator Message Identification (BIMI) is an industry-wide standards effort to use brand logos as indicators to help email recipients recognize and avoid fraudulent messages.

[IETF: Brand Indicators for Message Identification (BIMI)](https://datatracker.ietf.org/doc/draft-brand-indicators-for-message-identification/)

License
=======
This plugin is released under the GNU General Public License Version 3
or later (http://www.gnu.org/licenses/gpl.html).

Install
=======
* Place this plugin folder into plugins directory of Roundcube
* Add bimi to $config['plugins'] in your Roundcube config

Config
======
The default config file is plugins/bimi/config.inc.php.dist
Rename this to plugins/bimi/config.inc.php

73 changes: 73 additions & 0 deletions plugins/bimi/bimi.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

/**
* BIMI
*
* Plugin to display Display Brand Indicators for Message Identification (BIMI) icons
* for contacts/addresses that do not have a photo image.
*
* @license GNU GPLv3+
* @author Craig Andrews <[email protected]>
* @website http://roundcube.net
*/
class bimi extends rcube_plugin
{
public $task = 'addressbook';


/**
* Plugin initialization.
*/
function init()
{
$this->add_hook('contact_photo', [$this, 'contact_photo']);
}

/**
* 'contact_photo' hook handler to inject a bimi image
*/
function contact_photo($args)
{
// pre-conditions, exit if photo already exists or invalid input
if (!empty($args['url']) || !empty($args['data'])
|| (empty($args['record']) && empty($args['email']))
) {
return $args;
}

$rcmail = rcmail::get_instance();

// supporting edit/add action may be tricky, let's not do this
if ($rcmail->action == 'show' || $rcmail->action == 'photo') {
$email = !empty($args['email']) ? $args['email'] : null;

if (!$email && $args['record']) {
$addresses = rcube_addressbook::get_col_values('email', $args['record'], true);
if (!empty($addresses)) {
$email = $addresses[0];
}
}

if ($email) {
require_once __DIR__ . '/bimi_engine.php';
$bimi_image = new bimi_engine($email);

if ($rcmail->action == 'show') {
// set photo URL
if (($icon = $bimi_image->getBinary()) && ($icon = base64_encode($icon))) {
$mimetype = $bimi_image->getMimetype();
$args['url'] = sprintf('data:%s;base64,%s', $mimetype, $icon);
}
}
else {
// send the icon to the browser
if ($bimi_image->sendOutput()) {
exit;
}
}
}
}

return $args;
}
}
137 changes: 137 additions & 0 deletions plugins/bimi/bimi_engine.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<?php

/**
* @license GNU GPLv3+
* @author Craig Andrews <[email protected]>
*/
class bimi_engine
{
private string $email;
private ?string $binary;

const MIME_TYPE = 'image/svg+xml';
const CACHE_NULL_VALUE = 'NOT FOUND';

/**
* Class constructor
*
* @param string $email email address
*/
public function __construct($email)
{
$this->email = $email;
$this->retrieve();
}

/**
* Returns image mimetype
*/
public function getMimetype()
{
return self::MIME_TYPE;
}

/**
* Returns the image in binary form
*/
public function getBinary()
{
return $this->binary;
}

/**
* Sends the image to the browser
*/
public function sendOutput()
{
if ($this->binary) {
$rcmail = rcmail::get_instance();
$rcmail->output->future_expire_header(10 * 60);

header('Content-Type: ' . self::MIME_TYPE);
header('Content-Size: ' . strlen($this->binary));
echo $this->binary;

return true;
}

return false;
}

/**
* BIMI retriever
*/
private function retrieve()
{
if (preg_match('/.*@(.*)/', $this->email, $matches)) {
do {
$domain = $matches[1];
$this->binary = $this->cache_get_bimi_image($domain);
// If there's no BIMI at the subdomain, check the parent domain
}
while($this->binary == null && preg_match('/.*?\.(.*)/', $domain, $matches));
}
else {
$this->binary = null;
}
}

/**
* Using the cache, given a domain, returns the BIMI image. The image is always SVG XML. Returns null if no image could be retrieved.
*/
private function cache_get_bimi_image(string $domain): ?string
{
$rcmail = rcmail::get_instance();
$cache = $rcmail->get_cache_shared('bimi');
if ($cache && $cached_data=$cache->get($domain)) {
if ($cached_data==self::CACHE_NULL_VALUE) {
return null;
}
else {
return $cached_data;
}
}
else {
$data = $this->get_bimi_image($domain);
$cached_data=$data == null ? self::CACHE_NULL_VALUE : $data;
if ($cache) {
$cache->set($domain, $cached_data);
}
return $data;
}
}

/**
* Given a domain, returns the BIMI image. The image is always SVG XML. Returns null if no image could be retrieved.
*/
private function get_bimi_image(string $domain): ?string
{
if ($bimi_url = $this->get_bimi_url($domain)) {
$rcmail = rcmail::get_instance();
$client = $rcmail->get_http_client();
$res = $client->request('GET', $bimi_url);
if ( $res->getStatusCode() == 200 && $res->hasHeader('Content-Type') && strcasecmp($res->getHeader('Content-Type')[0], self::MIME_TYPE) == 0) {
$svg = $res->getBody()->getContents();
$svg = rcmail_attachment_handler::svg_filter($svg);
return $svg;
}
}
return null;
}

/**
* Given a domain, returns the BIMI URL or null if there no such domain or the domain doesn't have a BIMI record.
*/
private function get_bimi_url(string $domain): ?string
{
$bimi_record = dns_get_record("default._bimi.".$domain, DNS_TXT);
if ($bimi_record && sizeof($bimi_record) >= 1 && array_key_exists('txt', $bimi_record[0])) {
$bimi_record_value = $bimi_record[0]['txt'];
if (preg_match('@v=BIMI1(?:;|$)@i', $bimi_record_value, $svg) && preg_match('@l=(https://.+?)(?:;|$)@', $bimi_record_value, $matches)) {
$bimi_url = $matches[1];
return $bimi_url;
}
}
return null;
}
}
4 changes: 4 additions & 0 deletions plugins/bimi/config.inc.php.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?php

// Type of IMAP indexes cache. Supported values: 'db', 'apc' and 'memcache' or 'memcached'.
$config['bimi_cache'] = 'db';

0 comments on commit 4c1207c

Please sign in to comment.