[Web] Customize app menu and logo; Fix #671

master
André 2017-10-21 10:07:06 +02:00
parent a35bf76154
commit 81775765d8
17 changed files with 407 additions and 10 deletions

View File

@ -129,6 +129,7 @@ $tfa_data = get_tfa();
<a href="#fwdhosts" class="list-group-item"><?=$lang['admin']['forwarding_hosts'];?></a> <a href="#fwdhosts" class="list-group-item"><?=$lang['admin']['forwarding_hosts'];?></a>
<a href="#f2bparams" class="list-group-item"><?=$lang['admin']['f2b_parameters'];?></a> <a href="#f2bparams" class="list-group-item"><?=$lang['admin']['f2b_parameters'];?></a>
<a href="#relayhosts" class="list-group-item">Relayhosts</a> <a href="#relayhosts" class="list-group-item">Relayhosts</a>
<a href="#customize" class="list-group-item"><?=$lang['admin']['customize'];?></a>
<a href="#top" class="list-group-item" style="border-top:1px dashed #dadada"> <?=$lang['admin']['to_top'];?></a> <a href="#top" class="list-group-item" style="border-top:1px dashed #dadada"> <?=$lang['admin']['to_top'];?></a>
</div> </div>
</div> </div>
@ -260,9 +261,7 @@ $tfa_data = get_tfa();
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="private_key_file"><?=$lang['admin']['private_key'];?>:</label> <label for="private_key_file"><?=$lang['admin']['private_key'];?>:</label>
<textarea class="form-control" rows="5" name="private_key_file" id="private_key_file" required placeholder="-----BEGIN RSA PRIVATE KEY----- <textarea class="form-control" rows="5" name="private_key_file" id="private_key_file" required placeholder="-----BEGIN RSA KEY-----"></textarea>
XYZ
-----END RSA PRIVATE KEY-----"></textarea>
</div> </div>
<button class="btn btn-default" id="add_item" data-id="dkim_import" data-api-url='add/dkim_import' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-plus"></span> <?=$lang['admin']['import'];?></button> <button class="btn btn-default" id="add_item" data-id="dkim_import" data-api-url='add/dkim_import' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-plus"></span> <?=$lang['admin']['import'];?></button>
</form> </form>
@ -376,6 +375,82 @@ XYZ
</form> </form>
</div> </div>
</div> </div>
<span class="anchor" id="customize"></span>
<div class="panel panel-default">
<div class="panel-heading"><?=$lang['admin']['customize'];?></div>
<div class="panel-body">
<legend><?=$lang['admin']['change_logo'];?></legend>
<p class="help-block"><?=$lang['admin']['logo_info'];?></p>
<form class="form-inline" role="form" method="post" enctype="multipart/form-data">
<p>
<input type="file" name="main_logo" class="filestyle" data-buttonName="btn-default" data-buttonText="Select" accept="image/gif, image/jpeg, image/pjpeg, image/x-png, image/png, image/svg+xml">
<button name="submit_main_logo" type="submit" class="btn btn-success"><span class="glyphicon glyphicon-cloud-upload"></span> <?=$lang['admin']['upload'];?></button>
</p>
</form>
<?php
if ($main_logo = customize('get', 'main_logo')):
$specs = customize('get', 'main_logo_specs');
?>
<div class="row">
<div class="col-sm-3">
<div class="thumbnail">
<img class="img-thumbnail" src="<?=$main_logo;?>" alt="mailcow logo">
<div class="caption">
<span class="label label-info"><?=$specs['geometry']['width'];?>x<?=$specs['geometry']['height'];?> px</span>
<span class="label label-info"><?=$specs['mimetype'];?></span>
<span class="label label-info"><?=$specs['fileSize'];?></span>
</div>
</div>
<hr>
<form class="form-inline" role="form" method="post">
<p><button name="reset_main_logo" type="submit" class="btn btn-xs btn-default"><?=$lang['admin']['reset_default'];?></button></p>
</form>
</div>
</div>
<?php
endif;
?>
<legend><?=$lang['admin']['app_links'];?></legend>
<p class="help-block"><?=$lang['admin']['merged_vars_hint'];?></p>
<form class="form-inline" data-id="app_links" role="form" method="post">
<table class="table table-condensed" style="width:1%;white-space: nowrap;" id="app_link_table">
<tr>
<th><?=$lang['admin']['app_name'];?></th>
<th><?=$lang['admin']['link'];?></th>
<th>&nbsp;</th>
</tr>
<?php
$app_links = customize('get', 'app_links');
foreach ($app_links as $row) {
foreach ($row as $key => $val):
?>
<tr>
<td><input class="input-sm form-control" data-id="app_links" type="text" name="app" required value="<?=$key;?>"></td>
<td><input class="input-sm form-control" data-id="app_links" type="text" name="href" required value="<?=$val;?>"></td>
<td><a href="#" role="button" class="btn btn-xs btn-default" type="button"><?=$lang['admin']['remove_row'];?></a></td>
</tr>
<?php
endforeach;
}
foreach ($MAILCOW_APPS as $app):
?>
<tr>
<td><input class="input-sm form-control" value="<?=htmlspecialchars($app['name']);?>" disabled></td>
<td><input class="input-sm form-control" value="<?=htmlspecialchars($app['link']);?>" disabled></td>
<td>&nbsp;</td>
</tr>
<?php
endforeach;
?>
</table>
<div class="btn-group">
<button class="btn btn-success" id="edit_selected" data-item="admin" data-id="app_links" data-reload="no" data-api-url='edit/app_links' data-api-attr='{}' href="#"><?=$lang['admin']['save'];?></button>
<button class="btn btn-default" type="button" id="add_app_link_row"><?=$lang['admin']['add_row'];?></button>
</div>
</form>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -53,3 +53,7 @@ body.modal-open {
top: 65px; top: 65px;
z-index: 1; z-index: 1;
} }
.thumbnail img {
min-height:100px;
height:100px;
}

View File

@ -102,3 +102,12 @@ legend {
-o-user-select: none; -o-user-select: none;
user-select: none; user-select: none;
} }
.navbar .navbar-brand {
padding-top: 5px;
}
.navbar .navbar-brand img {
height: 40px;
}
.mailcow-logo img {
max-width: 250px;
}

View File

@ -6,10 +6,14 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/modals/footer.php';
<script src="/js/bootstrap-switch.min.js"></script> <script src="/js/bootstrap-switch.min.js"></script>
<script src="/js/bootstrap-slider.min.js"></script> <script src="/js/bootstrap-slider.min.js"></script>
<script src="/js/bootstrap-select.min.js"></script> <script src="/js/bootstrap-select.min.js"></script>
<script src="/js/bootstrap-filestyle.min.js"></script>
<script src="/js/notifications.min.js"></script> <script src="/js/notifications.min.js"></script>
<script src="/js/u2f-api.js"></script> <script src="/js/u2f-api.js"></script>
<script src="/js/api.js"></script> <script src="/js/api.js"></script>
<script> <script>
$(window).scroll(function() {
sessionStorage.scrollTop = $(this).scrollTop();
});
// Select language and reopen active URL without POST // Select language and reopen active URL without POST
function setLang(sel) { function setLang(sel) {
$.post( "<?= $_SERVER['REQUEST_URI']; ?>", {lang: sel} ); $.post( "<?= $_SERVER['REQUEST_URI']; ?>", {lang: sel} );
@ -192,6 +196,9 @@ $(document).ready(function() {
// CSRF // CSRF
$('<input type="hidden" value="<?= $_SESSION['CSRF']['TOKEN']; ?>">').attr('id', 'csrf_token').attr('name', 'csrf_token').appendTo('form'); $('<input type="hidden" value="<?= $_SESSION['CSRF']['TOKEN']; ?>">').attr('id', 'csrf_token').attr('name', 'csrf_token').appendTo('form');
if (sessionStorage.scrollTop != "undefined") {
$(window).scrollTop(sessionStorage.scrollTop);
}
}); });
</script> </script>

View File

@ -0,0 +1,180 @@
<?php
function customize($_action, $_item, $_data = null) {
global $redis;
global $lang;
switch ($_action) {
case 'add':
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'] = array(
'type' => 'danger',
'msg' => sprintf($lang['danger']['access_denied'])
);
return false;
}
switch ($_item) {
case 'main_logo':
if (in_array($_data['main_logo']['type'], array('image/gif', 'image/jpeg', 'image/pjpeg', 'image/x-png', 'image/png', 'image/svg+xml'))) {
try {
if (file_exists($_data['main_logo']['tmp_name']) !== true) {
$_SESSION['return'] = array(
'type' => 'danger',
'msg' => 'Cannot validate image file: Temporary file not found'
);
return false;
}
$image = new Imagick($_data['main_logo']['tmp_name']);
if ($image->valid() !== true) {
$_SESSION['return'] = array(
'type' => 'danger',
'msg' => 'Cannot validate image file'
);
return false;
}
$image->destroy();
}
catch (ImagickException $e) {
$image->destroy();
$_SESSION['return'] = array(
'type' => 'danger',
'msg' => 'Cannot validate image file'
);
return false;
}
}
else {
$_SESSION['return'] = array(
'type' => 'danger',
'msg' => 'Invalid mime type'
);
return false;
}
try {
$redis->Set('MAIN_LOGO', 'data:' . $_data['main_logo']['type'] . ';base64,' . base64_encode(file_get_contents($_data['main_logo']['tmp_name'])));
}
catch (RedisException $e) {
$_SESSION['return'] = array(
'type' => 'danger',
'msg' => 'Redis: '.$e
);
return false;
}
$_SESSION['return'] = array(
'type' => 'success',
'msg' => 'File uploaded successfully'
);
break;
}
break;
case 'edit':
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'] = array(
'type' => 'danger',
'msg' => sprintf($lang['danger']['access_denied'])
);
return false;
}
switch ($_item) {
case 'app_links':
$apps = (array)$_data['app'];
$links = (array)$_data['href'];
$out = array();
if (count($apps) == count($links)) {;
for ($i = 0; $i < count($apps); $i++) {
$out[] = array($apps[$i] => $links[$i]);
}
try {
$redis->set('APP_LINKS', json_encode($out));
}
catch (RedisException $e) {
$_SESSION['return'] = array(
'type' => 'danger',
'msg' => 'Redis: '.$e
);
return false;
}
}
$_SESSION['return'] = array(
'type' => 'success',
'msg' => 'Saved changes to app links'
);
break;
}
break;
case 'delete':
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'] = array(
'type' => 'danger',
'msg' => sprintf($lang['danger']['access_denied'])
);
return false;
}
switch ($_item) {
case 'main_logo':
try {
if ($redis->del('MAIN_LOGO')) {
$_SESSION['return'] = array(
'type' => 'success',
'msg' => 'Reset default logo'
);
return true;
}
}
catch (RedisException $e) {
$_SESSION['return'] = array(
'type' => 'danger',
'msg' => 'Redis: '.$e
);
return false;
}
break;
}
break;
case 'get':
switch ($_item) {
case 'app_links':
try {
$app_links = json_decode($redis->get('APP_LINKS'), true);
}
catch (RedisException $e) {
$_SESSION['return'] = array(
'type' => 'danger',
'msg' => 'Redis: '.$e
);
return false;
}
return ($app_links) ? $app_links : false;
break;
case 'main_logo':
try {
return $redis->get('MAIN_LOGO');
}
catch (RedisException $e) {
$_SESSION['return'] = array(
'type' => 'danger',
'msg' => 'Redis: '.$e
);
return false;
}
break;
case 'main_logo_specs':
try {
$image = new Imagick();
$img_data = explode('base64,', customize('get', 'main_logo'));
if ($img_data[1]) {
$image->readImageBlob(base64_decode($img_data[1]));
}
return $image->identifyImage();
}
catch (ImagickException $e) {
$_SESSION['return'] = array(
'type' => 'danger',
'msg' => 'Error: Imagick exception while reading image'
);
return false;
}
break;
}
break;
}
}

View File

@ -1291,11 +1291,11 @@ function mailbox($_action, $_type, $_data = null) {
$port1 = (!empty($_data['port1'])) ? $_data['port1'] : $is_now['port1']; $port1 = (!empty($_data['port1'])) ? $_data['port1'] : $is_now['port1'];
$password1 = (!empty($_data['password1'])) ? $_data['password1'] : $is_now['password1']; $password1 = (!empty($_data['password1'])) ? $_data['password1'] : $is_now['password1'];
$host1 = (!empty($_data['host1'])) ? $_data['host1'] : $is_now['host1']; $host1 = (!empty($_data['host1'])) ? $_data['host1'] : $is_now['host1'];
$subfolder2 = (!empty($_data['subfolder2'])) ? $_data['subfolder2'] : ''; $subfolder2 = (isset($_data['subfolder2'])) ? $_data['subfolder2'] : $is_now['subfolder2'];
$enc1 = (!empty($_data['enc1'])) ? $_data['enc1'] : $is_now['enc1']; $enc1 = (!empty($_data['enc1'])) ? $_data['enc1'] : $is_now['enc1'];
$mins_interval = (!empty($_data['mins_interval'])) ? $_data['mins_interval'] : $is_now['mins_interval']; $mins_interval = (!empty($_data['mins_interval'])) ? $_data['mins_interval'] : $is_now['mins_interval'];
$exclude = (!empty($_data['exclude'])) ? $_data['exclude'] : ''; $exclude = (!empty($_data['exclude'])) ? $_data['exclude'] : '';
$maxage = (!empty($_data['maxage'])) ? $_data['maxage'] : $is_now['maxage']; $maxage = (isset($_data['maxage']) && $_data['maxage'] != "") ? intval($_data['maxage']) : $is_now['maxage'];
} }
else { else {
$_SESSION['return'] = array( $_SESSION['return'] = array(

View File

@ -39,7 +39,7 @@
<span class="icon-bar"></span> <span class="icon-bar"></span>
<span class="icon-bar"></span> <span class="icon-bar"></span>
</button> </button>
<a class="navbar-brand" href="/"><img height="32" alt="mailcow-logo" style="margin-top: -5px;" src="/img/cow_mailcow.svg"></a> <a class="navbar-brand" href="/"><img alt="mailcow-logo" src="<?=($main_logo = customize('get', 'main_logo')) ? $main_logo : '/img/cow_mailcow.svg';?>"></a>
</div> </div>
<div id="navbar" class="navbar-collapse collapse"> <div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right"> <ul class="nav navbar-nav navbar-right">
@ -102,6 +102,14 @@
<li title="<?= htmlspecialchars($app['description']); ?>"><a href="<?= htmlspecialchars($app['link']); ?>"><?= htmlspecialchars($app['name']); ?></a></li> <li title="<?= htmlspecialchars($app['description']); ?>"><a href="<?= htmlspecialchars($app['link']); ?>"><?= htmlspecialchars($app['name']); ?></a></li>
<?php <?php
endforeach; endforeach;
$app_links = customize('get', 'app_links');
foreach ($app_links as $row) {
foreach ($row as $key => $val):
?>
<li><a href="<?= htmlspecialchars($val); ?>"><?= htmlspecialchars($key); ?></a></li>
<?php
endforeach;
}
?> ?>
</ul> </ul>
</li> </li>

View File

@ -63,6 +63,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/lang/lang.en.php';
include $_SERVER['DOCUMENT_ROOT'] . '/lang/lang.'.$_SESSION['mailcow_locale'].'.php'; include $_SERVER['DOCUMENT_ROOT'] . '/lang/lang.'.$_SESSION['mailcow_locale'].'.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.mailbox.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.mailbox.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.customize.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.domain_admin.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.domain_admin.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.policy.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.policy.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.dkim.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.dkim.inc.php';

View File

@ -53,6 +53,7 @@ if (isset($_SESSION['mailcow_cc_role']) && session_check() === false) {
'msg' => 'Form token invalid or timed out' 'msg' => 'Form token invalid or timed out'
); );
$_POST = array(); $_POST = array();
$_FILES = array();
} }
// Handle logouts // Handle logouts

View File

@ -63,4 +63,14 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
unset_tfa_key($_POST); unset_tfa_key($_POST);
} }
} }
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admin") {
if (isset($_POST["submit_main_logo"])) {
if ($_FILES['main_logo']['error'] == 0) {
customize('add', 'main_logo', $_FILES);
}
}
if (isset($_POST["reset_main_logo"])) {
customize('delete', 'main_logo');
}
}
?> ?>

View File

@ -23,7 +23,7 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"><span class="glyphicon glyphicon-user" aria-hidden="true"></span> <?= $lang['login']['login']; ?></div> <div class="panel-heading"><span class="glyphicon glyphicon-user" aria-hidden="true"></span> <?= $lang['login']['login']; ?></div>
<div class="panel-body"> <div class="panel-body">
<div class="text-center"><img style="max-width: 250px;" src="/img/cow_mailcow.svg" alt="mailcow"></div> <div class="text-center mailcow-logo"><img src="<?=($main_logo = customize('get', 'main_logo')) ? $main_logo : '/img/cow_mailcow.svg';?>" alt="mailcow"></div>
<legend>mailcow UI</legend> <legend>mailcow UI</legend>
<form method="post" autofill="off"> <form method="post" autofill="off">
<div class="form-group"> <div class="form-group">
@ -72,6 +72,14 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
<a href="<?= htmlspecialchars($app['link']); ?>" role="button" title="<?= htmlspecialchars($app['description']); ?>" class="btn btn-lg btn-default"><?= htmlspecialchars($app['name']); ?></a>&nbsp; <a href="<?= htmlspecialchars($app['link']); ?>" role="button" title="<?= htmlspecialchars($app['description']); ?>" class="btn btn-lg btn-default"><?= htmlspecialchars($app['name']); ?></a>&nbsp;
<?php <?php
endforeach; endforeach;
$app_links = customize('get', 'app_links');
foreach ($app_links as $row) {
foreach ($row as $key => $val):
?>
<a href="<?= htmlspecialchars($val); ?>" role="button" class="btn btn-lg btn-default"><?= htmlspecialchars($key); ?></a>&nbsp;
<?php
endforeach;
}
?> ?>
</div> </div>
</div> </div>

View File

@ -730,6 +730,24 @@ jQuery(function($){
} }
}); });
}) })
function add_table_row(table_id) {
var row = $('<tr />');
cols = '<td><input class="input-sm form-control" data-id="app_links" type="text" name="app" required></td>';
cols += '<td><input class="input-sm form-control" data-id="app_links" type="text" name="href" required></td>';
cols += '<td><a href="#" role="button" class="btn btn-xs btn-default" type="button">Remove row</a></td>';
row.append(cols);
table_id.append(row);
}
$('#app_link_table').on('click', 'tr a', function (e) {
e.preventDefault();
$(this).parents('tr').remove();
});
$('#add_app_link_row').click(function() {
add_table_row($('#app_link_table'));
});
}); });
$(window).load(function(){ $(window).load(function(){

View File

@ -14,7 +14,7 @@ $(document).ready(function() {
}); });
return o; return o;
}; };
// Collect values of input fields with name "multi_select" an same data-id to js array multi_data[data-id] // Collect values of input fields with name "multi_select" and same data-id to js array multi_data[data-id]
var multi_data = []; var multi_data = [];
$(document).on('change', 'input[name=multi_select]:checkbox', function() { $(document).on('change', 'input[name=multi_select]:checkbox', function() {
if ($(this).is(':checked') && $(this).data('id')) { if ($(this).is(':checked') && $(this).data('id')) {
@ -105,7 +105,8 @@ $(document).ready(function() {
url: '/api/v1/' + api_url, url: '/api/v1/' + api_url,
jsonp: false, jsonp: false,
complete: function(data) { complete: function(data) {
// var reponse = (JSON.parse(data.responseText)); var response = (data.responseText);
// alert(response);
// console.log(reponse.type); // console.log(reponse.type);
// console.log(reponse.msg); // console.log(reponse.msg);
window.location = window.location.href.split("#")[0]; window.location = window.location.href.split("#")[0];

File diff suppressed because one or more lines are too long

View File

@ -1795,6 +1795,48 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
)); ));
} }
break; break;
case "app_links":
if (isset($_POST['attr'])) {
$attr = (array)json_decode($_POST['attr'], true);
if (is_array($attr)) {
if (customize('edit', 'app_links', $attr) === false) {
if (isset($_SESSION['return'])) {
echo json_encode($_SESSION['return']);
}
else {
echo json_encode(array(
'type' => 'error',
'msg' => 'Edit failed'
));
}
exit();
}
else {
if (isset($_SESSION['return'])) {
echo json_encode($_SESSION['return']);
}
else {
echo json_encode(array(
'type' => 'success',
'msg' => 'Task completed'
));
}
}
}
else {
echo json_encode(array(
'type' => 'error',
'msg' => 'Incomplete post data'
));
}
}
else {
echo json_encode(array(
'type' => 'error',
'msg' => 'Incomplete post data'
));
}
break;
case "relayhost": case "relayhost":
if (isset($_POST['items']) && isset($_POST['attr'])) { if (isset($_POST['items']) && isset($_POST['attr'])) {
$items = (array)json_decode($_POST['items'], true); $items = (array)json_decode($_POST['items'], true);

View File

@ -512,3 +512,14 @@ $lang['success']['relayhost_removed'] = "Relayhost %s wurde entfernt";
$lang['success']['relayhost_added'] = "Relayhost %s wurde hinzugefügt"; $lang['success']['relayhost_added'] = "Relayhost %s wurde hinzugefügt";
$lang['admin']['relay_from'] = "Absenderadresse"; $lang['admin']['relay_from'] = "Absenderadresse";
$lang['admin']['relay_run'] = "Test durchführen"; $lang['admin']['relay_run'] = "Test durchführen";
$lang['admin']['customize'] = "Anpassung";
$lang['admin']['change_logo'] = "Logo ändern";
$lang['admin']['logo_info'] = "Die hochgeladene Grafik wird für die Navigationsleiste auf eine Höhe von 40px skaliert. Für die Startseite ist eine Skalierung auf eine maximale Breite von 250px programmiert. Eine frei skalierbare Grafik (etwa SVG) wird empfohlen.";
$lang['admin']['upload'] = "Hochladen";
$lang['admin']['app_links'] = "App Links";
$lang['admin']['app_name'] = "App Name";
$lang['admin']['link'] = "Link";
$lang['admin']['remove_row'] = "Zeile entfernen";
$lang['admin']['add_row'] = "Zeile hinzufügen";
$lang['admin']['reset_default'] = "Auf Standard zurücksetzen";
$lang['admin']['merged_vars_hint'] = 'Ausgegraute Zeilen wurden aus der Datei <code>vars.inc.(local.)php</code> gelesen und können nicht mittels UI verändert werden.';

View File

@ -525,3 +525,15 @@ $lang['success']['relayhost_removed'] = "Relayhost %s has been removed";
$lang['success']['relayhost_added'] = "Relayhost %s has been added"; $lang['success']['relayhost_added'] = "Relayhost %s has been added";
$lang['admin']['relay_from'] = '"From:" address'; $lang['admin']['relay_from'] = '"From:" address';
$lang['admin']['relay_run'] = "Run test"; $lang['admin']['relay_run'] = "Run test";
$lang['admin']['customize'] = "Customize";
$lang['admin']['change_logo'] = "Change logo";
$lang['admin']['logo_info'] = "Your image will be scaled to a height of 40px for the top navigation bar and a max. width of 250px for the start page. A scalable graphic is highly recommended.";
$lang['admin']['upload'] = "Upload";
$lang['admin']['app_links'] = "App links";
$lang['admin']['app_name'] = "App name";
$lang['admin']['link'] = "Link";
$lang['admin']['remove_row'] = "Remove row";
$lang['admin']['add_row'] = "Add row";
$lang['admin']['reset_default'] = "Reset to default";
$lang['admin']['merged_vars_hint'] = 'Greyed out rows were merged from <code>vars.inc.(local.)php</code> and cannot be modified.';