Closes #180: IMAP inbox synching (ui)

This commit is contained in:
Benjamin Gamard 2018-02-27 20:05:10 +01:00
parent 797a987e2b
commit 7ded510625
8 changed files with 187 additions and 215 deletions

View File

@ -1,2 +1,7 @@
insert into T_CONFIG(CFG_ID_C, CFG_VALUE_C) values('INBOX_ENABLED', 'false');
insert into T_CONFIG(CFG_ID_C, CFG_VALUE_C) values('INBOX_HOSTNAME', '');
insert into T_CONFIG(CFG_ID_C, CFG_VALUE_C) values('INBOX_PORT', '');
insert into T_CONFIG(CFG_ID_C, CFG_VALUE_C) values('INBOX_USERNAME', '');
insert into T_CONFIG(CFG_ID_C, CFG_VALUE_C) values('INBOX_PASSWORD', '');
insert into T_CONFIG(CFG_ID_C, CFG_VALUE_C) values('INBOX_TAG', '');
update T_CONFIG set CFG_VALUE_C = '17' where CFG_ID_C = 'DB_VERSION';

View File

@ -4,82 +4,29 @@
* Settings inbox page controller.
*/
angular.module('docs').controller('SettingsInbox', function($scope, $rootScope, Restangular) {
// Get the app configuration
Restangular.one('app').get().then(function (data) {
$rootScope.app = data;
$scope.general = {
default_language: data.default_language
}
// Get the inbox configuration
Restangular.one('app/config_inbox').get().then(function (data) {
$scope.inbox = data;
});
// Enable/disable guest login
$scope.changeGuestLogin = function (enabled) {
Restangular.one('app').post('guest_login', {
enabled: enabled
}).then(function () {
$scope.app.guest_login = enabled;
});
};
// Fetch the current theme configuration
Restangular.one('theme').get().then(function (data) {
$scope.theme = data;
$rootScope.appName = $scope.theme.name;
// Get the tags
Restangular.one('tag/list').get().then(function(data) {
$scope.tags = data.tags;
});
// Update the theme
$scope.update = function () {
$scope.theme.name = $scope.theme.name.length === 0 ? 'Sismics Docs' : $scope.theme.name;
Restangular.one('theme').post('', $scope.theme).then(function () {
var stylesheet = $('#theme-stylesheet')[0];
stylesheet.href = stylesheet.href.replace(/\?.*|$/, '?' + new Date().getTime());
$rootScope.appName = $scope.theme.name;
});
// Save the inbox configuration
$scope.editInboxConfig = function () {
return Restangular.one('app').post('config_inbox', $scope.inbox);
};
// Send an image
$scope.sendingImage = false;
$scope.sendImage = function (type, image) {
// Build the payload
var formData = new FormData();
formData.append('image', image);
// Send the file
var done = function() {
$scope.$apply(function() {
$scope.sendingImage = false;
$scope[type] = null;
$scope.testInboxConfig = function () {
$scope.testLoading = true;
$scope.testResult = undefined;
$scope.editInboxConfig().then(function () {
Restangular.one('app').post('test_inbox').then(function (data) {
$scope.testResult = data;
$scope.testLoading = false;
});
};
$scope.sendingImage = true;
$.ajax({
type: 'PUT',
url: '../api/theme/image/' + type,
data: formData,
cache: false,
contentType: false,
processData: false,
success: function() {
done();
},
error: function() {
done();
}
});
};
// Load SMTP config
Restangular.one('app/config_smtp').get().then(function (data) {
$scope.smtp = data;
});
// Edit SMTP config
$scope.editSmtpConfig = function () {
Restangular.one('app').post('config_smtp', $scope.smtp);
};
// Edit general config
$scope.editGeneralConfig = function () {
Restangular.one('app').post('config', $scope.general);
};
});

View File

@ -361,6 +361,20 @@
"smtp_password": "SMTP password",
"smtp_updated": "SMTP configuration updated successfully"
},
"inbox": {
"title": "Inbox scanning",
"message": "By enabling this feature, the system while periodically scan the specified inbox for new emails and automatically import them.",
"enabled": "Enable inbox scanning",
"hostname": "IMAP hostname",
"port": "IMAP port (143 or 993)",
"username": "IMAP username",
"password": "IMAP password",
"tag": "Tag added to imported documents",
"test": "Test the parameters",
"last_sync": "Last synchronization: {{ data.date | date }}, {{ data.count }} message{{ data.count > 1 ? 's' : '' }} imported",
"test_success": "The connection to the inbox is successful ({{ count }} message{{ count > 1 ? 's' : '' }})",
"test_fail": "An error occurred while connecting to the inbox, please check the parameters"
},
"log": {
"title": "Server logs",
"date": "Date",

View File

@ -245,9 +245,10 @@
"menu_groups": "Groupes",
"menu_vocabularies": "Vocabulaires",
"menu_configuration": "Configuration",
"menu_inbox": "Scanning de boîte de réception",
"menu_server_logs": "Logs serveur",
"user": {
"title": "<small>Gestion des</small> utilisateurs",
"title": "Gestion des utilisateurs",
"add_user": "Ajouter un utilisateur",
"username": "Nom d'utilisateur",
"create_date": "Date de création",
@ -257,8 +258,8 @@
"delete_user_message": "Etes-vous sûr de vouloir supprimer cet utilisateur ? Tous les documents, fichiers et tags associés seront supprimés",
"edit_user_failed_title": "Cet utilisateur existe déjà",
"edit_user_failed_message": "Ce nom d'utilisateur est déjà pris par un autre utilisateur",
"edit_user_title": "<small>Modifier</small> \"{{ username }}\"",
"add_user_title": "<small>Ajouter un</small> utilisateur",
"edit_user_title": "Modifier \"{{ username }}\"",
"add_user_title": "Ajouter un utilisateur",
"username": "Nom d'utilisateur",
"email": "E-mail",
"groups": "Groupes",
@ -273,15 +274,15 @@
}
},
"workflow": {
"title": "<small>Configuration des</small> workflows",
"title": "Configuration des workflows",
"add_workflow": "Ajouter un workflow",
"name": "Nom",
"create_date": "Date de création",
"edit": {
"delete_workflow_title": "Supprimer le workflow",
"delete_workflow_message": "Voulez-vous vraiment supprimer ce workflow ? Les workflows en cours d'exécution ne seront pas supprimés",
"edit_workflow_title": "<small>Modifier</small> \"{{ name }}\"",
"add_workflow_title": "<small>Ajouter un</small> workflow",
"edit_workflow_title": "Modifier \"{{ name }}\"",
"add_workflow_title": "Ajouter un workflow",
"name": "Nom",
"name_placeholder": "Nom de l'étape ou description",
"drag_help": "Glisser et déplacer pour réordonner les étapes",
@ -296,7 +297,7 @@
"security": {
"enable_totp": "Activer l'authentification en deux étapes",
"enable_totp_message": "Assurez-vous d'avoir une application compatible TOTP sur votre téléphone prête à être configurée",
"title": "<small>Authentification</small> en deux étapes",
"title": "Authentification en deux étapes",
"message_1": "L'authentification en deux étapes vous permet d'ajouter une couche de sécurité supplémentaire sur votre compte <strong>{{ appName }}</strong>.<br/>Avant d'activer cette fonctionnalité, assurez-vous d'avoir une application compatible TOTP sur votre téléphone :",
"message_google_authenticator": "Pour Android, iOS, et Blackberry: <a href=\"https://support.google.com/accounts/answer/1066447\" target=\"_blank\">Google Authenticator</a>",
"message_duo_mobile": "Pour Android et iOS: <a href=\"https://guide.duo.com/third-party-accounts\" target=\"_blank\">Duo Mobile</a>",
@ -313,7 +314,7 @@
}
},
"group": {
"title": "<small>Gestion des</small> groupes",
"title": "Gestion des groupes",
"add_group": "Ajouter un groupe",
"name": "Nom",
"edit": {
@ -321,8 +322,8 @@
"delete_group_message": "Etes-vous sûr de vouloir supprimer ce groupe ?",
"edit_group_failed_title": "Ce groupe existe déjà",
"edit_group_failed_message": "Ce nom de groupe est déjà pris par un autre groupe",
"edit_group_title": "<small>Modifier</small> \"{{ name }}\"",
"add_group_title": "<small>Ajouter un</small> groupe",
"edit_group_title": "Modifier \"{{ name }}\"",
"add_group_title": "Ajouter un groupe",
"name": "Nom",
"parent_group": "Groupe parent",
"search_group": "Rechercher un groupe",
@ -332,17 +333,19 @@
}
},
"account": {
"title": "<small>Compte</small> utilisateur",
"title": "Compte utilisateur",
"password": "Mot de passe",
"password_confirm": "Mot de passe (confirmation)",
"updated": "Compte mis à jour avec succès"
},
"config": {
"title_guest_access": "<small>Accès</small> invité",
"title_guest_access": "Accès invité",
"message_guest_access": "L'accès invité est un mode dans lequel quiconque peut accéder à {{ appName }} sans mot de passe.<br/>Comme un utilisateur normal, l'invité ne pourra accéder qu'aux documents auquel il a accès via les permissions.<br/>",
"enable_guest_access": "Activer l'accès invité",
"disable_guest_access": "Désactiver l'accès invité",
"title_theme": "<small>Personnalisation</small> de l'interface",
"title_theme": "Personnalisation de l'interface",
"title_general": "Configuration générale",
"default_language": "Langage par défaut pour les nouveaux documents",
"application_name": "Nom de l'application",
"main_color": "Couleur principale",
"custom_css": "CSS personnalisée",
@ -350,7 +353,7 @@
"logo": "Logo (Taille carrée)",
"background_image": "Image de fond",
"uploading_image": "Envoi de l'image...",
"title_smtp": "<small>Configuration</small> email",
"title_smtp": "Configuration email",
"smtp_hostname": "Hôte SMTP",
"smtp_port": "Port SMTP",
"smtp_from": "Email d'envoi",
@ -358,14 +361,28 @@
"smtp_password": "Mot de passe SMTP",
"smtp_updated": "Configuration SMTP mise à jour avec succès"
},
"inbox": {
"title": "Scanning de boîte de réception",
"message": "En activant cette fonctionnalité, le système scanne périodiquement la boîte de réception spécifiée pour rechercher de nouveaux messages et les importer automatiquement.",
"enabled": "Activer le scan de boîte de réception",
"hostname": "Nom d'hôte IMAP",
"port": "Port IMAP (143 ou 993)",
"username": "Nom d'utilisateur IMAP",
"password": "Mot de passe IMAP",
"tag": "Tag ajouté aux documents importés",
"test": "Tester les paramètres",
"last_sync": "Dernière synchronisation : {{ data.date | date }}, {{ data.count }} message{{ data.count> 1 ? 's' : '' }} importé{{ data.count> 1 ? 's' : '' }}",
"test_success": "La connexion à la boîte de réception est réussie ({{ count }} message{{ count> 1 ? 's' : '' }})",
"test_fail": "Une erreur est survenue lors de la connexion à la boîte de réception, veuillez vérifier les paramètres"
},
"log": {
"title": "<small>Logs</small> serveur",
"title": "Logs serveur",
"date": "Date",
"tag": "Tag",
"message": "Message"
},
"session": {
"title": "Sessions <small>ouvertes</small>",
"title": "Sessions ouvertes",
"created_date": "Date de création",
"last_connection_date": "Date de dernière connexion",
"user_agent": "Depuis",
@ -375,7 +392,7 @@
"clear": "Fermeture des autres sessions"
},
"vocabulary": {
"title": "<small>Entrées</small> de vocabulaire",
"title": "Entrées de vocabulaire",
"choose_vocabulary": "Choisissez un vocabulaire à modifier",
"type": "Type",
"coverage": "Couverture",

View File

@ -49,6 +49,7 @@
"search_clear": "Очистить",
"any_language": "Любой язык",
"add_document": "Добавить документ",
"import_eml": "Импорт из электронной почты (формат EML)",
"tags": "Теги",
"no_tags": "Нет тегов",
"no_documents": "В базе данных нет документа",
@ -244,6 +245,7 @@
"menu_groups": "Группы",
"menu_vocabularies": "Словари",
"menu_configuration": "Конфигурация",
"menu_inbox": "Входящие сканирования",
"menu_server_logs": "Журналы сервера",
"user": {
"title": "<small>Управление</small> пользователями",
@ -342,6 +344,8 @@
"enable_guest_access": "Включить гостевой доступ",
"disable_guest_access": "Отключить гостевой доступ",
"title_theme": "<small>Настройка</small> темы",
"title_general": "Общая конфигурация",
"default_language": "Язык по умолчанию для новых документов",
"application_name": "Имя приложения",
"main_color": "Основной цвет",
"custom_css": "Пользовательские CSS",
@ -357,6 +361,20 @@
"smtp_password": "Пароль SMTP",
"smtp_updated": "Конфигурация SMTP успешно обновлена"
},
"inbox": {
"title": "Входящие сканирования",
"message": "Включив эту функцию, система периодически проверяет указанный почтовый ящик для новых писем и автоматически импортирует их.",
"enabled": "Включить сканирование входящих сообщений",
"hostname": "Имя хоста IMAP",
"port": "Порт IMAP (143 или 993)",
"username": "Имя пользователя IMAP",
"password": "Пароль IMAP",
"tag": "Тег добавлен в импортированные документы",
"test": "Проверить параметры",
"last_sync": "Последняя синхронизация: {{data.date | date}}, {{data.count}} импортировано",
"test_success": "Соединение с почтовым ящиком успешное ({{count}} сообщение)",
"test_fail": "При подключении к папке «Входящие» произошла ошибка, проверьте параметры"
},
"log": {
"title": "Cерверные <small>журналы</small>",
"date": "Дата",
@ -390,6 +408,11 @@
"sent_title": "Обратная связь отправлена",
"sent_message": "Спасибо за ваш отзыв! Это поможет нам улучшить работу Sismics Docs."
},
"import": {
"title": "Импорт",
"error_quota": "Достигнутый предел квоты, обратитесь к администратору, чтобы увеличить свою квоту",
"error_general": "При попытке импортировать файл произошла ошибка, убедитесь, что он является допустимым файлом EML"
},
"app_share": {
"main": "Запросить ссылку на общий документ для доступа",
"403": {
@ -415,7 +438,8 @@
"Group": "Группа",
"Tag": "Тег",
"User": "Пользователь",
"RouteModel": "Workflow"
"RouteModel": "Модель рабочего процесса",
"Route": "Workflow"
},
"selectrelation": {
"typeahead": "Введите название документа"

View File

@ -245,6 +245,7 @@
"menu_groups": "群组",
"menu_vocabularies": "词条",
"menu_configuration": "配置",
"menu_inbox": "收件箱扫描",
"menu_server_logs": "服务器日志",
"user": {
"title": "用户 <small>管理</small>",
@ -343,6 +344,8 @@
"enable_guest_access": "激活游客权限",
"disable_guest_access": "不激活游客权限",
"title_theme": "主题 <small>定制</small>",
"title_general": "一般配置",
"default_language": "新文档的默认语言",
"application_name": "申请名",
"main_color": "主颜色",
"custom_css": "用户样式表",
@ -358,6 +361,20 @@
"smtp_password": "SMTP密码",
"smtp_updated": "SMTP组态更新成功"
},
"inbox": {
"title": "收件箱扫描",
"message": "通过启用此功能,系统会定期扫描指定收件箱中的新电子邮件并自动导入它们。",
"enabled": "启用收件箱扫描",
"hostname": "IMAP主机名",
"port": "IMAP端口143或993",
"username": "IMAP用户名",
"password": "IMAP密码",
"tag": "标签添加到导入的文档",
"test": "测试参数",
"last_sync": "上次同步:{{ data.date | date }}{{ data.count }}消息导入",
"test_success": "与收件箱的连接成功({{count}}消息)",
"test_fail": "连接到收件箱时发生错误,请检查参数"
},
"log": {
"title": "服务器 <small>日志</small>",
"date": "日期",

View File

@ -245,6 +245,7 @@
"menu_groups": "群組",
"menu_vocabularies": "詞條",
"menu_configuration": "配置",
"menu_inbox": "收件箱掃描",
"menu_server_logs": "服務器日誌",
"user": {
"title": "用戶 <small>管理</small>",
@ -343,6 +344,8 @@
"enable_guest_access": "激活遊客權限",
"disable_guest_access": "不激活遊客權限",
"title_theme": "主題 <small>定制</small>",
"title_general": "一般配置",
"default_language": "新文檔的默認語言",
"application_name": "申請名",
"main_color": "主顏色",
"custom_css": "用戶樣式表",
@ -358,6 +361,20 @@
"smtp_password": "SMTP密碼",
"smtp_updated": "SMTP配置已成功更新"
},
"inbox": {
"title": "收件箱掃描",
"message": "通過啟用此功能,系統會定期掃描指定收件箱中的新電子郵件並自動導入它們。",
"enabled": "啟用收件箱掃描",
"hostname": "IMAP主機名",
"port": "IMAP端口143或993",
"username": "IMAP用戶名",
"password": "IMAP密碼",
"tag": "標籤添加到導入的文檔",
"test": "測試參數",
"last_sync": "上次同步:{{ data.date | date }}{{data.count}}消息導入",
"test_success": "與收件箱的連接成功({{ count }} 消息)",
"test_fail": "連接到收件箱時發生錯誤,請檢查參數"
},
"log": {
"title": "服務器 <small>日誌</small>",
"date": "日期",

View File

@ -1,145 +1,76 @@
<h2>
<span translate="settings.config.title_guest_access"></span>
<span class="label" ng-class="{ 'label-success': app.guest_login, 'label-danger': !app.guest_login }">
{{ app.guest_login ? 'enabled' : 'disabled' | translate }}
<span translate="settings.inbox.title"></span>
<span class="label" ng-show="inbox"
ng-class="{ 'label-success': inbox.enabled, 'label-danger': !inbox.enabled }">
{{ inbox.enabled ? 'enabled' : 'disabled' | translate }}
</span>
</h2>
<p translate="settings.config.message_guest_access" translate-values="{ appName: appName }">
</p>
<div ng-if="app">
<button ng-if="!app.guest_login" class="btn btn-primary" ng-click="changeGuestLogin(true)">{{ 'settings.config.enable_guest_access' | translate }}</button>
<button ng-if="app.guest_login" class="btn btn-danger" ng-click="changeGuestLogin(false)">{{ 'settings.config.disable_guest_access' | translate }}</button>
</div>
<p translate="settings.inbox.message"></p>
<p ng-show="inbox.last_sync" translate="settings.inbox.last_sync" translate-values="{ data: inbox.last_sync }"></p>
<p ng-show="inbox.last_sync.error" class="text-danger">{{ inbox.last_sync.error }}</p>
<h2 translate="settings.config.title_general"></h2>
<form class="form-horizontal" name="generalForm" novalidate>
<div class="form-group" ng-class="{ 'has-error': !generalForm.defaultLanguage.$valid && generalForm.$dirty }">
<label class="col-sm-2 control-label" for="defaultLanguage">{{ 'settings.config.default_language' | translate }}</label>
<form class="form-horizontal" name="inboxForm" novalidate>
<div class="form-group" ng-class="{ 'has-error': !smtpForm.hostname.$valid && smtpForm.$dirty }">
<label class="col-sm-2 control-label" for="inboxEnabled">{{ 'settings.inbox.enabled' | translate }}</label>
<div class="col-sm-7">
<select name="defaultLanguage" class="form-control" id="defaultLanguage" ng-model="general.default_language">
<option ng-repeat="language in acceptedLanguages" value="{{ language.key }}">{{ language.label }}</option>
<input name="hostname" type="checkbox" id="inboxEnabled" ng-model="inbox.enabled" />
</div>
</div>
<div class="form-group" ng-class="{ 'has-error': !inboxForm.hostname.$valid && inboxForm.$dirty }">
<label class="col-sm-2 control-label" for="inboxHostname">{{ 'settings.inbox.hostname' | translate }}</label>
<div class="col-sm-7">
<input name="hostname" type="text" class="form-control" id="inboxHostname" ng-model="inbox.hostname" />
</div>
</div>
<div class="form-group" ng-class="{ 'has-error': !inboxForm.port.$valid && inboxForm.$dirty }">
<label class="col-sm-2 control-label" for="inboxPort">{{ 'settings.inbox.port' | translate }}</label>
<div class="col-sm-7">
<input name="port" type="number" class="form-control" id="inboxPort" ng-model="inbox.port" />
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="inboxUsername">{{ 'settings.inbox.username' | translate }}</label>
<div class="col-sm-7">
<input name="username" type="text" class="form-control" id="inboxUsername" ng-model="inbox.username" />
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="inboxPassword">{{ 'settings.inbox.password' | translate }}</label>
<div class="col-sm-7">
<input name="password" type="password" class="form-control" id="inboxPassword" ng-model="inbox.password" />
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="inboxPassword">{{ 'settings.inbox.tag' | translate }}</label>
<div class="col-sm-7">
<select class="form-control" ng-model="inbox.tag" id="inboxTag">
<option value=""></option>
<option ng-repeat="tag0 in tags"
ng-if="tag0.id != tag.id"
ng-selected="inbox.tag == tag0.id"
value="{{ tag0.id }}">{{ tag0.name }}</option>
</select>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-primary" ng-click="editGeneralConfig()" ng-disabled="!generalForm.$valid">
<button type="submit" class="btn btn-primary" ng-click="editInboxConfig()" ng-disabled="!inboxForm.$valid">
<span class="glyphicon glyphicon-pencil"></span> {{ 'save' | translate }}
</button>
<button type="submit" class="btn btn-warning" ng-click="testInboxConfig()">
<span class="glyphicon glyphicon-pencil"></span> {{ 'settings.inbox.test' | translate }}
<img ng-if="testLoading" src="img/loader.gif" />
</button>
</div>
</div>
</form>
<h2 translate="settings.config.title_theme"></h2>
<form class="form-horizontal" name="editColorForm" novalidate>
<div class="form-group">
<label class="col-sm-2 control-label" for="inputName">{{ 'settings.config.application_name' | translate }}</label>
<div class="col-sm-8">
<input type="text" class="form-control"
id="inputName" ng-model="theme.name" ng-blur="update()" />
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="inputColor">{{ 'settings.config.main_color' | translate }}</label>
<div class="col-sm-1">
<span colorpicker class="btn btn-default" id="inputColor" on-hide="update()"
data-color="" ng-model="theme.color" ng-style="{ 'background': theme.color }">&nbsp;&nbsp;&nbsp;</span>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="inputCss">{{ 'settings.config.custom_css' | translate }}</label>
<div class="col-sm-8">
<textarea class="form-control" rows="6" ng-attr-placeholder="{{ 'settings.config.custom_css_placeholder' | translate }}"
id="inputCss" ng-model="theme.css" ng-blur="update()"></textarea>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="inputLogo">{{ 'settings.config.logo' | translate }}</label>
<div class="col-sm-2">
<input type="file" ngf-select ngf-accept="'image/gif,image/png,image/jpg,image/jpeg'"
class="form-control" id="inputLogo" ng-model="logo" ng-disabled="sendingImage" />
</div>
<div class="col-sm-2">
<button class="btn btn-default" ng-click="sendImage('logo', logo)" ng-disabled="sendingImage || !logo">
<span class="glyphicon glyphicon-save"></span>
{{ 'send' | translate }}
</button>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="inputBackground">{{ 'settings.config.background_image' | translate }}</label>
<div class="col-sm-2">
<input type="file" ngf-select ngf-accept="'image/gif,image/png,image/jpg,image/jpeg'"
class="form-control" id="inputBackground" ng-model="background" ng-disabled="sendingImage" />
</div>
<div class="col-sm-2">
<button class="btn btn-default" ng-click="sendImage('background', background)" ng-disabled="sendingImage || !background">
<span class="glyphicon glyphicon-save"></span>
{{ 'send' | translate }}
</button>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label"></label>
<div class="col-sm-10">
<p class="form-control-static text-info" ng-if="sendingImage">
{{ 'settings.config.uploading_image' | translate }}
</p>
</div>
</div>
</form>
<h2 translate="settings.config.title_smtp"></h2>
<form class="form-horizontal" name="smtpForm" novalidate>
<div class="form-group" ng-show="smtp.hasOwnProperty('hostname')" ng-class="{ 'has-error': !smtpForm.hostname.$valid && smtpForm.$dirty }">
<label class="col-sm-2 control-label" for="smtpHostname">{{ 'settings.config.smtp_hostname' | translate }}</label>
<div class="col-sm-7">
<input name="hostname" type="text" ng-disabled="!smtp.hasOwnProperty('hostname')" class="form-control" id="smtpHostname" ng-model="smtp.hostname" />
</div>
</div>
<div class="form-group" ng-show="smtp.hasOwnProperty('port')" ng-class="{ 'has-error': !smtpForm.port.$valid && smtpForm.$dirty }">
<label class="col-sm-2 control-label" for="smtpPort">{{ 'settings.config.smtp_port' | translate }}</label>
<div class="col-sm-7">
<input name="port" type="number" ng-disabled="!smtp.hasOwnProperty('port')" class="form-control" id="smtpPort" ng-model="smtp.port" />
</div>
</div>
<div class="form-group" ng-show="smtp.hasOwnProperty('username')">
<label class="col-sm-2 control-label" for="smtpUsername">{{ 'settings.config.smtp_username' | translate }}</label>
<div class="col-sm-7">
<input name="username" type="text" ng-disabled="!smtp.hasOwnProperty('username')" class="form-control" id="smtpUsername" ng-model="smtp.username" />
</div>
</div>
<div class="form-group" ng-show="smtp.hasOwnProperty('password')">
<label class="col-sm-2 control-label" for="smtpPassword">{{ 'settings.config.smtp_password' | translate }}</label>
<div class="col-sm-7">
<input name="password" type="password" ng-disabled="!smtp.hasOwnProperty('password')" class="form-control" id="smtpPassword" ng-model="smtp.password" />
</div>
</div>
<div class="form-group" ng-class="{ 'has-error': !smtpForm.from.$valid && smtpForm.$dirty }">
<label class="col-sm-2 control-label" for="smtpFrom">{{ 'settings.config.smtp_from' | translate }}</label>
<div class="col-sm-7">
<input name="from" type="email" class="form-control" id="smtpFrom" ng-model="smtp.from" />
</div>
<div class="col-sm-3">
<span class="help-block" ng-show="smtpForm.from.$error.email && smtpForm.$dirty">{{ 'validation.email' | translate }}</span>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-primary" ng-click="editSmtpConfig()" ng-disabled="!smtpForm.$valid">
<span class="glyphicon glyphicon-pencil"></span> {{ 'save' | translate }}
</button>
</div>
</div>
</form>
<div class="alert"
ng-class="{ 'alert-success': testResult.count >= 0, 'alert-danger': testResult.count == -1 }"
ng-show="testResult">{{ testResult.count >= 0 ? 'settings.inbox.test_success' : 'settings.inbox.test_fail' | translate: { count: testResult.count } }}</div>