* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { cloneDeep, countBy, pick } from 'lodash';
+import { cloneDeep, countBy, pick, trim } from 'lodash';
import { mockQualityProfile, mockRuleDetails, mockRuleRepository } from '../../helpers/testMocks';
import { RuleRepository } from '../../types/coding-rules';
import { RawIssuesResponse } from '../../types/issues';
import { SearchRulesQuery } from '../../types/rules';
-import { Rule, RuleActivation, RuleDetails } from '../../types/types';
+import {
+ Rule,
+ RuleActivation,
+ RuleDescriptionSections,
+ RuleDetails,
+ RulesUpdateRequest
+} from '../../types/types';
import { getFacet } from '../issues';
import {
bulkActivateRules,
SearchQualityProfilesParameters,
SearchQualityProfilesResponse
} from '../quality-profiles';
-import { getRuleDetails, getRulesApp, searchRules } from '../rules';
+import { getRuleDetails, getRulesApp, searchRules, updateRule } from '../rules';
interface FacetFilter {
languages?: string;
lang: 'c',
langName: 'C',
name: 'Awsome C rule'
+ }),
+ mockRuleDetails({
+ key: 'rule5',
+ type: 'VULNERABILITY',
+ lang: 'py',
+ langName: 'Python',
+ name: 'Awsome Python rule',
+ descriptionSections: [
+ { key: RuleDescriptionSections.INTRODUCTION, content: 'Introduction to this rule' },
+ { key: RuleDescriptionSections.HOW_TO_FIX, content: 'This how to fix' },
+ {
+ key: RuleDescriptionSections.RESOURCES,
+ content: 'Some link <a href="http://example.com">Awsome Reading</a>'
+ }
+ ]
+ }),
+ mockRuleDetails({
+ key: 'rule6',
+ type: 'VULNERABILITY',
+ lang: 'py',
+ langName: 'Python',
+ name: 'Bad Python rule',
+ descriptionSections: undefined
})
];
+ (updateRule as jest.Mock).mockImplementation(this.handleUpdateRule);
(searchRules as jest.Mock).mockImplementation(this.handleSearchRules);
(getRuleDetails as jest.Mock).mockImplementation(this.handleGetRuleDetails);
(searchQualityProfiles as jest.Mock).mockImplementation(this.handleSearchQualityProfiles);
this.rules = cloneDeep(this.defaultRules);
}
+ allRulesCount() {
+ return this.rules.length;
+ }
+
allRulesName() {
return this.rules.map(r => r.name);
}
return this.reply({ actives: parameters.actives ? [] : undefined, rule });
};
+ handleUpdateRule = (data: RulesUpdateRequest): Promise<RuleDetails> => {
+ const rule = this.rules.find(r => r.key === data.key);
+ if (rule === undefined) {
+ return Promise.reject({
+ errors: [{ msg: `No rule has been found for id ${data.key}` }]
+ });
+ }
+ const template = this.rules.find(r => r.key === rule.templateKey);
+
+ // Lets not convert the md to html in test.
+ rule.mdDesc = data.markdown_description !== undefined ? data.markdown_description : rule.mdDesc;
+ rule.htmlDesc =
+ data.markdown_description !== undefined ? data.markdown_description : rule.htmlDesc;
+ rule.mdNote = data.markdown_note !== undefined ? data.markdown_note : rule.mdNote;
+ rule.htmlNote = data.markdown_note !== undefined ? data.markdown_note : rule.htmlNote;
+ rule.name = data.name !== undefined ? data.name : rule.name;
+ if (template && data.params) {
+ rule.params = [];
+ data.params.split(';').forEach(param => {
+ const parts = param.split('=');
+ const paramsDef = template.params?.find(p => p.key === parts[0]);
+ rule.params?.push({
+ key: parts[0],
+ type: paramsDef?.type || 'STRING',
+ defaultValue: trim(parts[1], '" '),
+ htmlDesc: paramsDef?.htmlDesc
+ });
+ });
+ }
+
+ rule.remFnBaseEffort =
+ data.remediation_fn_base_effort !== undefined
+ ? data.remediation_fn_base_effort
+ : rule.remFnBaseEffort;
+ rule.remFnType =
+ data.remediation_fn_type !== undefined ? data.remediation_fn_type : rule.remFnType;
+ rule.severity = data.severity !== undefined ? data.severity : rule.severity;
+ rule.status = data.status !== undefined ? data.status : rule.status;
+ rule.tags = data.tags !== undefined ? data.tags.split(';') : rule.tags;
+
+ return this.reply(rule);
+ };
+
handleSearchRules = ({ facets, languages, p, ps, rule_key }: SearchRulesQuery) => {
const countFacet = (facets || '').split(',').map((facet: keyof Rule) => {
const facetCount = countBy(this.rules.map(r => r[FACET_RULE_MAP[facet] || facet] as string));
import { getJSON, post, postJSON } from '../helpers/request';
import { GetRulesAppResponse, SearchRulesResponse } from '../types/coding-rules';
import { SearchRulesQuery } from '../types/rules';
-import { RuleActivation, RuleDetails } from '../types/types';
+import { RuleActivation, RuleDetails, RulesUpdateRequest } from '../types/types';
export function getRulesApp(): Promise<GetRulesAppResponse> {
return getJSON('/api/rules/app').catch(throwGlobalError);
return post('/api/rules/delete', parameters).catch(throwGlobalError);
}
-export function updateRule(data: {
- key: string;
- markdown_description?: string;
- markdown_note?: string;
- name?: string;
- params?: string;
- remediation_fn_base_effort?: string;
- remediation_fn_type?: string;
- remediation_fy_gap_multiplier?: string;
- severity?: string;
- status?: string;
- tags?: string;
-}): Promise<RuleDetails> {
+export function updateRule(data: RulesUpdateRequest): Promise<RuleDetails> {
return postJSON('/api/rules/update', data).then(r => r.rule, throwGlobalError);
}
expect(screen.queryByRole('link', { name: 'Hot hotspot' })).not.toBeInTheDocument();
});
-it('should show open rule', async () => {
+it('should show open rule with default description section', async () => {
renderCodingRulesApp(undefined, 'coding_rules?open=rule1');
expect(
await screen.findByRole('heading', { level: 3, name: 'Awsome java rule' })
).toBeInTheDocument();
+ expect(
+ screen.getByRole('region', { name: 'coding_rules.description_section.title.root_cause' })
+ ).toBeInTheDocument();
+});
+
+it('should show open rule with no description', async () => {
+ renderCodingRulesApp(undefined, 'coding_rules?open=rule6');
+ expect(
+ await screen.findByRole('heading', { level: 3, name: 'Bad Python rule' })
+ ).toBeInTheDocument();
+ expect(screen.getByText('issue.external_issue_description.Bad Python rule')).toBeInTheDocument();
+});
+
+it('should show open rule advance section', async () => {
+ renderCodingRulesApp(undefined, 'coding_rules?open=rule5');
+ expect(
+ await screen.findByRole('heading', { level: 3, name: 'Awsome Python rule' })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole('region', { name: 'coding_rules.description_section.title.introduction' })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole('region', { name: 'coding_rules.description_section.title.how_to_fix' })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole('region', { name: 'coding_rules.description_section.title.resources' })
+ ).toBeInTheDocument();
+ // Check that we render plain html
+ expect(screen.getByRole('link', { name: 'Awsome Reading' })).toBeInTheDocument();
+});
+
+it('should be able to extend the rule description', async () => {
+ const user = userEvent.setup();
+ handler.setIsAdmin();
+ renderCodingRulesApp(undefined, 'coding_rules?open=rule5');
+ expect(
+ await screen.findByRole('heading', { level: 3, name: 'Awsome Python rule' })
+ ).toBeInTheDocument();
+
+ // Add
+ await user.click(screen.getByRole('button', { name: 'coding_rules.extend_description' }));
+ expect(screen.getByRole('textbox')).toBeInTheDocument();
+ await user.click(screen.getByRole('textbox'));
+ await user.keyboard('TEST DESC');
+ await user.click(screen.getByRole('button', { name: 'save' }));
+ expect(await screen.findByText('TEST DESC')).toBeInTheDocument();
+
+ // Edit
+ await user.click(screen.getByRole('button', { name: 'coding_rules.extend_description' }));
+ await user.click(screen.getByRole('textbox'));
+ await user.keyboard('{Control>}A{/Control}NEW DESC');
+ await user.click(screen.getByRole('button', { name: 'save' }));
+ expect(await screen.findByText('NEW DESC')).toBeInTheDocument();
+
+ //Cancel
+ await user.click(screen.getByRole('button', { name: 'coding_rules.extend_description' }));
+ await user.dblClick(screen.getByRole('textbox'));
+ await user.keyboard('DIFFERENCE');
+ await user.click(screen.getByRole('button', { name: 'cancel' }));
+ expect(await screen.findByText('NEW DESC')).toBeInTheDocument();
+
+ //Remove
+ await user.click(screen.getByRole('button', { name: 'coding_rules.extend_description' }));
+ await user.click(screen.getByRole('button', { name: 'remove' }));
+ await user.click(within(screen.getByRole('dialog')).getByRole('button', { name: 'remove' }));
+ await waitFor(() => expect(screen.queryByText('NEW DESC')).not.toBeInTheDocument());
});
it('should list all rules', async () => {
await user.click(await screen.findByRole('link', { name: 'coding_rules.activate_in…' }));
const dialog = screen.getByRole('dialog', {
- name: 'coding_rules.activate_in_quality_profile (4 coding_rules._rules)'
+ name: `coding_rules.activate_in_quality_profile (${handler.allRulesCount()} coding_rules._rules)`
});
expect(dialog).toBeInTheDocument();
await user.click(await screen.findByRole('link', { name: 'coding_rules.activate_in…' }));
dialogScreen = within(
screen.getByRole('dialog', {
- name: 'coding_rules.activate_in_quality_profile (4 coding_rules._rules)'
+ name: `coding_rules.activate_in_quality_profile (${handler.allRulesCount()} coding_rules._rules)`
})
);
await user.click(dialogScreen.getByRole('textbox', { name: 'coding_rules.activate_in' }));
await user.click(await screen.findByRole('link', { name: 'coding_rules.deactivate_in…' }));
const dialogScreen = within(
screen.getByRole('dialog', {
- name: 'coding_rules.deactivate_in_quality_profile (4 coding_rules._rules)'
+ name: `coding_rules.deactivate_in_quality_profile (${handler.allRulesCount()} coding_rules._rules)`
})
);
await user.click(dialogScreen.getByRole('textbox', { name: 'coding_rules.deactivate_in' }));
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { sortBy } from 'lodash';
import * as React from 'react';
import { updateRule } from '../../../api/rules';
import FormattingTips from '../../../components/common/FormattingTips';
import { Button, ResetButtonLink } from '../../../components/controls/buttons';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { sanitizeString } from '../../../helpers/sanitize';
-import { RuleDetails } from '../../../types/types';
+import {
+ Dict,
+ RuleDescriptionSection,
+ RuleDescriptionSections,
+ RuleDetails
+} from '../../../types/types';
import RemoveExtendedDescriptionModal from './RemoveExtendedDescriptionModal';
+const SECTION_ORDER: Dict<number> = {
+ [RuleDescriptionSections.INTRODUCTION]: 0,
+ [RuleDescriptionSections.ROOT_CAUSE]: 1,
+ [RuleDescriptionSections.ASSESS_THE_PROBLEM]: 2,
+ [RuleDescriptionSections.HOW_TO_FIX]: 3,
+ [RuleDescriptionSections.RESOURCES]: 4
+};
+
interface Props {
canWrite: boolean | undefined;
onChange: (newRuleDetails: RuleDetails) => void;
});
};
- renderDescription = () => (
+ sortedDescriptionSections(ruleDetails: RuleDetails) {
+ return sortBy(
+ ruleDetails.descriptionSections,
+ s => SECTION_ORDER[s.key] || Object.keys(SECTION_ORDER).length
+ );
+ }
+
+ renderExtendedDescription = () => (
<div id="coding-rules-detail-description-extra">
{this.props.ruleDetails.htmlNote !== undefined && (
<div
</div>
);
+ renderDescription(section: RuleDescriptionSection) {
+ return (
+ <section
+ aria-label={translate('coding_rules.description_section.title', section.key)}
+ className="coding-rules-detail-description rule-desc markdown"
+ key={section.key}
+ /* eslint-disable-next-line react/no-danger */
+ dangerouslySetInnerHTML={{ __html: sanitizeString(section.content) }}
+ />
+ );
+ }
+
render() {
const { ruleDetails } = this.props;
const hasDescription = !ruleDetails.isExternal || ruleDetails.type !== 'UNKNOWN';
return (
<div className="js-rule-description">
- {hasDescription && ruleDetails.htmlDesc !== undefined ? (
- <div
- className="coding-rules-detail-description rule-desc markdown"
- // eslint-disable-next-line react/no-danger
- dangerouslySetInnerHTML={{ __html: sanitizeString(ruleDetails.htmlDesc) }}
- />
+ {hasDescription &&
+ ruleDetails.descriptionSections &&
+ ruleDetails.descriptionSections.length > 0 ? (
+ this.sortedDescriptionSections(ruleDetails).map(this.renderDescription)
) : (
<div className="coding-rules-detail-description rule-desc markdown">
{translateWithParameters('issue.external_issue_description', ruleDetails.name)}
{!ruleDetails.templateKey && (
<div className="coding-rules-detail-description coding-rules-detail-description-extra">
- {!this.state.descriptionForm && this.renderDescription()}
+ {!this.state.descriptionForm && this.renderExtendedDescription()}
{this.state.descriptionForm && this.props.canWrite && this.renderForm()}
</div>
)}
import { shallow } from 'enzyme';
import * as React from 'react';
import { createRule } from '../../../../api/rules';
-import { mockRule, mockRuleDetailsParameter } from '../../../../helpers/testMocks';
+import { mockRuleDetails, mockRuleDetailsParameter } from '../../../../helpers/testMocks';
import { submit, waitAndUpdate } from '../../../../helpers/testUtils';
import CustomRuleFormModal from '../CustomRuleFormModal';
onClose={jest.fn()}
onDone={jest.fn()}
templateRule={{
- ...mockRule({
+ ...mockRuleDetails({
params: [
mockRuleDetailsParameter(),
mockRuleDetailsParameter({ key: '2', type: 'TEXT', htmlDesc: undefined })
const wrapper = shallowRender();
const ruleChange = {
createdAt: '2019-02-01',
+ descriptionSections: [],
key: 'foo',
name: 'Foo',
repo: 'bar',
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2022 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import { shallow } from 'enzyme';
-import * as React from 'react';
-import { change, click, waitAndUpdate } from '../../../../helpers/testUtils';
-import { RuleDetails } from '../../../../types/types';
-import RuleDetailsDescription from '../RuleDetailsDescription';
-
-jest.mock('../../../../api/rules', () => ({
- updateRule: jest.fn().mockResolvedValue('updatedrule')
-}));
-
-const RULE: RuleDetails = {
- key: 'squid:S1133',
- repo: 'squid',
- name: 'Deprecated code should be removed',
- createdAt: '2013-07-26T09:40:51+0200',
- htmlDesc: '<p>Html Description</p>',
- mdNote: 'Md Note',
- severity: 'INFO',
- status: 'READY',
- lang: 'java',
- langName: 'Java',
- type: 'CODE_SMELL'
-};
-
-const EXTERNAL_RULE: RuleDetails = {
- createdAt: '2013-07-26T09:40:51+0200',
- key: 'external_xoo:OneExternalIssuePerLine',
- repo: 'external_xoo',
- name: 'xoo:OneExternalIssuePerLine',
- severity: 'MAJOR',
- status: 'READY',
- isExternal: true,
- type: 'UNKNOWN'
-};
-
-const EXTERNAL_RULE_WITH_DATA: RuleDetails = {
- key: 'external_xoo:OneExternalIssueWithDetailsPerLine',
- repo: 'external_xoo',
- name: 'One external issue per line',
- createdAt: '2018-05-31T11:19:51+0200',
- htmlDesc: '<p>Html Description</p>',
- severity: 'MAJOR',
- status: 'READY',
- isExternal: true,
- type: 'BUG'
-};
-
-it('should display correctly', () => {
- expect(getWrapper()).toMatchSnapshot();
- expect(getWrapper({ ruleDetails: EXTERNAL_RULE })).toMatchSnapshot();
- expect(getWrapper({ ruleDetails: EXTERNAL_RULE_WITH_DATA })).toMatchSnapshot();
-});
-
-it('should add extra description', async () => {
- const onChange = jest.fn();
- const wrapper = getWrapper({ canWrite: true, onChange });
- click(wrapper.find('#coding-rules-detail-extend-description'));
- expect(wrapper.find('textarea').exists()).toBe(true);
- change(wrapper.find('textarea'), 'new description');
- click(wrapper.find('#coding-rules-detail-extend-description-submit'));
- await waitAndUpdate(wrapper);
- expect(onChange).toBeCalledWith('updatedrule');
-});
-
-function getWrapper(props = {}) {
- return shallow(
- <RuleDetailsDescription canWrite={false} onChange={jest.fn()} ruleDetails={RULE} {...props} />
- );
-}
repo: 'squid',
name: 'Deprecated code should be removed',
createdAt: '2013-07-26T09:40:51+0200',
+ descriptionSections: [],
severity: 'INFO',
status: 'READY',
lang: 'java',
repo: 'external_xoo',
name: 'xoo:OneExternalIssuePerLine',
createdAt: '2018-05-31T11:22:13+0200',
+ descriptionSections: [],
severity: 'MAJOR',
status: 'READY',
scope: 'ALL',
repo: 'external_xoo',
name: 'One external issue per line',
createdAt: '2018-05-31T11:19:51+0200',
+ descriptionSections: [],
severity: 'MAJOR',
status: 'READY',
tags: ['tag'],
}
value={
Object {
- "label": "issue.type.CODE_SMELL",
- "value": "CODE_SMELL",
+ "label": "issue.type.BUG",
+ "value": "BUG",
}
}
/>
}
value={
Object {
- "label": "issue.type.CODE_SMELL",
- "value": "CODE_SMELL",
+ "label": "issue.type.BUG",
+ "value": "BUG",
}
}
/>
"defaultDebtRemFnType": "CONSTANT_ISSUE",
"defaultRemFnBaseEffort": "5min",
"defaultRemFnType": "CONSTANT_ISSUE",
+ "descriptionSections": Array [
+ Object {
+ "content": "<b>Why<b/> Because",
+ "key": "root_cause",
+ },
+ ],
"htmlDesc": "",
"isExternal": false,
"isTemplate": false,
"defaultDebtRemFnType": "CONSTANT_ISSUE",
"defaultRemFnBaseEffort": "5min",
"defaultRemFnType": "CONSTANT_ISSUE",
+ "descriptionSections": Array [
+ Object {
+ "content": "<b>Why<b/> Because",
+ "key": "root_cause",
+ },
+ ],
"htmlDesc": "",
"isExternal": false,
"isTemplate": false,
"defaultDebtRemFnType": "CONSTANT_ISSUE",
"defaultRemFnBaseEffort": "5min",
"defaultRemFnType": "CONSTANT_ISSUE",
+ "descriptionSections": Array [
+ Object {
+ "content": "<b>Why<b/> Because",
+ "key": "root_cause",
+ },
+ ],
"htmlDesc": "",
"isExternal": false,
"isTemplate": false,
"defaultDebtRemFnType": "CONSTANT_ISSUE",
"defaultRemFnBaseEffort": "5min",
"defaultRemFnType": "CONSTANT_ISSUE",
+ "descriptionSections": Array [
+ Object {
+ "content": "<b>Why<b/> Because",
+ "key": "root_cause",
+ },
+ ],
"htmlDesc": "",
"isExternal": false,
"isTemplate": false,
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should display correctly 1`] = `
-<div
- className="js-rule-description"
->
- <div
- className="coding-rules-detail-description rule-desc markdown"
- dangerouslySetInnerHTML={
- Object {
- "__html": "<p>Html Description</p>",
- }
- }
- />
- <div
- className="coding-rules-detail-description coding-rules-detail-description-extra"
- >
- <div
- id="coding-rules-detail-description-extra"
- />
- </div>
-</div>
-`;
-
-exports[`should display correctly 2`] = `
-<div
- className="js-rule-description"
->
- <div
- className="coding-rules-detail-description rule-desc markdown"
- >
- issue.external_issue_description.xoo:OneExternalIssuePerLine
- </div>
- <div
- className="coding-rules-detail-description coding-rules-detail-description-extra"
- >
- <div
- id="coding-rules-detail-description-extra"
- />
- </div>
-</div>
-`;
-
-exports[`should display correctly 3`] = `
-<div
- className="js-rule-description"
->
- <div
- className="coding-rules-detail-description rule-desc markdown"
- dangerouslySetInnerHTML={
- Object {
- "__html": "<p>Html Description</p>",
- }
- }
- />
- <div
- className="coding-rules-detail-description coding-rules-detail-description-extra"
- >
- <div
- id="coding-rules-detail-description-extra"
- />
- </div>
-</div>
-`;
rule={
Object {
"createdAt": "2013-07-26T09:40:51+0200",
+ "descriptionSections": Array [],
"key": "squid:S1133",
"lang": "java",
"langName": "Java",
ProfileInheritanceDetails,
Rule,
RuleActivation,
+ RuleDescriptionSections,
RuleDetails,
RuleParameter,
SnippetsByComponent,
repo: 'squid',
name: '".equals()" should not be used to test the values of "Atomic" classes',
createdAt: '2014-12-16T17:26:54+0100',
+ descriptionSections: [
+ {
+ key: RuleDescriptionSections.ROOT_CAUSE,
+ content: '<b>Why<b/> Because'
+ }
+ ],
htmlDesc: '',
mdDesc: '',
severity: 'MAJOR',
severity: string;
}
+export enum RuleDescriptionSections {
+ DEFAULT = 'default',
+ INTRODUCTION = 'introduction',
+ ROOT_CAUSE = 'root_cause',
+ ASSESS_THE_PROBLEM = 'assess_the_problem',
+ HOW_TO_FIX = 'how_to_fix',
+ RESOURCES = 'resources'
+}
+
+export interface RuleDescriptionSection {
+ key: RuleDescriptionSections;
+ content: string;
+}
+
+export interface RulesUpdateRequest {
+ key: string;
+ markdown_description?: string;
+ markdown_note?: string;
+ name?: string;
+ params?: string;
+ remediation_fn_base_effort?: string;
+ remediation_fn_type?: string;
+ remediation_fy_gap_multiplier?: string;
+ severity?: string;
+ status?: string;
+ tags?: string;
+}
+
export interface RuleDetails extends Rule {
createdAt: string;
debtOverloaded?: boolean;
defaultRemFnBaseEffort?: string;
defaultRemFnType?: string;
effortToFixDescription?: string;
+ descriptionSections?: RuleDescriptionSection[];
htmlDesc?: string;
htmlNote?: string;
internalKey?: string;
coding_rules.external_rule.engine=Rule provided by an external rule engine: {0}
+coding_rules.description_section.title.default=
+coding_rules.description_section.title.introduction=Introduction
+coding_rules.description_section.title.root_cause=Why is this an issue?
+coding_rules.description_section.title.assess_the_problem=Assess the risk?
+coding_rules.description_section.title.how_to_fix=How to fix it?
+coding_rules.description_section.title.resources=Resources
+
+
#------------------------------------------------------------------------------
#
# EMAIL CONFIGURATION