Skip to content
Snippets Groups Projects
Commit 0c6de8ca authored by Arthur Ulfeldt's avatar Arthur Ulfeldt Committed by GitHub
Browse files

Merge pull request #4793 from metabase/add-starttls-smtp-authentication

Add starttls smtp authentication
parents 66a042c6 78dcaf72
No related branches found
No related tags found
No related merge requests found
Showing
with 206 additions and 106 deletions
......@@ -6,10 +6,14 @@ import people from "metabase/admin/people/people";
import databases from "metabase/admin/databases/database";
import datamodel from "metabase/admin/datamodel/datamodel";
import permissions from "metabase/admin/permissions/permissions";
import settings from "metabase/admin/settings/settings";
export default {
import { combineReducers } from "metabase/lib/redux";
export default combineReducers({
databases,
datamodel: datamodel,
datamodel,
people,
permissions,
};
settings
})
......@@ -5,7 +5,7 @@ import { createSelector } from 'reselect';
// Database List
export const databases = state => state.databases.databases;
export const databases = state => state.admin.databases.databases;
export const getDatabasesSorted = createSelector(
[databases],
......@@ -19,5 +19,5 @@ export const hasSampleDataset = createSelector(
// Database Edit
export const getEditingDatabase = state => state.databases.editingDatabase;
export const getFormState = state => state.databases.formState;
export const getEditingDatabase = state => state.admin.databases.editingDatabase;
export const getFormState = state => state.admin.databases.formState;
......@@ -9,12 +9,15 @@ import { isFK } from "metabase/lib/types";
import { MetabaseApi, SegmentApi, MetricApi, RevisionsApi } from "metabase/services";
import { getEditingDatabase } from "./selectors";
function loadDatabaseMetadata(databaseId) {
return MetabaseApi.db_metadata({ 'dbId': databaseId });
}
// initializeMetadata
export const initializeMetadata = createThunkAction("INITIALIZE_METADATA", function(databaseId, tableId) {
export const INITIALIZE_METADATA = "metabase/admin/datamodel/INITIALIZE_METADATA";
export const initializeMetadata = createThunkAction(INITIALIZE_METADATA, function(databaseId, tableId) {
return async function(dispatch, getState) {
let databases, database;
try {
......@@ -43,7 +46,8 @@ export const initializeMetadata = createThunkAction("INITIALIZE_METADATA", funct
});
// fetchDatabaseIdfields
export const fetchDatabaseIdfields = createThunkAction("FETCH_IDFIELDS", function(databaseId) {
export const FETCH_IDFIELDS = "metabase/admin/datamodel/FETCH_IDFIELDS";
export const fetchDatabaseIdfields = createThunkAction(FETCH_IDFIELDS, function(databaseId) {
return async function(dispatch, getState) {
try {
let idfields = await MetabaseApi.db_idfields({ 'dbId': databaseId });
......@@ -58,7 +62,8 @@ export const fetchDatabaseIdfields = createThunkAction("FETCH_IDFIELDS", functio
});
// selectDatabase
export const selectDatabase = createThunkAction("SELECT_DATABASE", function(db) {
export const SELECT_DATABASE = "metabase/admin/datamodel/SELECT_DATABASE";
export const selectDatabase = createThunkAction(SELECT_DATABASE, function(db) {
return async function(dispatch, getState) {
try {
let database = await loadDatabaseMetadata(db.id);
......@@ -76,7 +81,8 @@ export const selectDatabase = createThunkAction("SELECT_DATABASE", function(db)
});
// selectTable
export const selectTable = createThunkAction("SELECT_TABLE", function(table) {
export const SELECT_TABLE = "metabase/admin/datamodel/SELECT_TABLE";
export const selectTable = createThunkAction(SELECT_TABLE, function(table) {
return function(dispatch, getState) {
// we also want to update our url to match our new state
dispatch(push('/admin/datamodel/database/'+table.db_id+'/table/'+table.id));
......@@ -86,7 +92,8 @@ export const selectTable = createThunkAction("SELECT_TABLE", function(table) {
});
// updateTable
export const updateTable = createThunkAction("UPDATE_TABLE", function(table) {
export const UPDATE_TABLE = "metabase/admin/datamodel/UPDATE_TABLE";
export const updateTable = createThunkAction(UPDATE_TABLE, function(table) {
return async function(dispatch, getState) {
try {
// make sure we don't send all the computed metadata
......@@ -109,9 +116,10 @@ export const updateTable = createThunkAction("UPDATE_TABLE", function(table) {
});
// updateField
export const updateField = createThunkAction("UPDATE_FIELD", function(field) {
export const UPDATE_FIELD = "metabase/admin/datamodel/UPDATE_FIELD";
export const updateField = createThunkAction(UPDATE_FIELD, function(field) {
return async function(dispatch, getState) {
const { datamodel: { editingDatabase } } = getState();
const editingDatabase = getEditingDatabase(getState());
try {
// make sure we don't send all the computed metadata
......@@ -138,7 +146,8 @@ export const updateField = createThunkAction("UPDATE_FIELD", function(field) {
});
// updateFieldSpecialType
export const updateFieldSpecialType = createThunkAction("UPDATE_FIELD_SPECIAL_TYPE", function(field) {
export const UPDATE_FIELD_SPECIAL_TYPE = "metabase/admin/datamodel/UPDATE_FIELD_SPECIAL_TYPE";
export const updateFieldSpecialType = createThunkAction(UPDATE_FIELD_SPECIAL_TYPE, function(field) {
return function(dispatch, getState) {
// If we are changing the field from a FK to something else, we should delete any FKs present
......@@ -157,7 +166,8 @@ export const updateFieldSpecialType = createThunkAction("UPDATE_FIELD_SPECIAL_TY
});
// updateFieldTarget
export const updateFieldTarget = createThunkAction("UPDATE_FIELD_TARGET", function(field) {
export const UPDATE_FIELD_TARGET = "metabase/admin/datamodel/UPDATE_FIELD_TARGET";
export const updateFieldTarget = createThunkAction(UPDATE_FIELD_TARGET, function(field) {
return function(dispatch, getState) {
// This function notes a change in the target of the target of a foreign key
dispatch(updateField(field));
......@@ -167,9 +177,10 @@ export const updateFieldTarget = createThunkAction("UPDATE_FIELD_TARGET", functi
});
// retireSegment
export const onRetireSegment = createThunkAction("RETIRE_SEGMENT", function(segment) {
export const RETIRE_SEGMENT = "metabase/admin/datamodel/RETIRE_SEGMENT";
export const onRetireSegment = createThunkAction(RETIRE_SEGMENT, function(segment) {
return async function(dispatch, getState) {
const { datamodel: { editingDatabase } } = getState();
const editingDatabase = getEditingDatabase(getState());
await SegmentApi.delete(segment);
MetabaseAnalytics.trackEvent("Data Model", "Retire Segment");
......@@ -179,9 +190,10 @@ export const onRetireSegment = createThunkAction("RETIRE_SEGMENT", function(segm
});
// retireMetric
export const onRetireMetric = createThunkAction("RETIRE_METRIC", function(metric) {
export const RETIRE_METRIC = "metabase/admin/datamodel/RETIRE_METRIC";
export const onRetireMetric = createThunkAction(RETIRE_METRIC, function(metric) {
return async function(dispatch, getState) {
const { datamodel: { editingDatabase } } = getState();
const editingDatabase = getEditingDatabase(getState());
await MetricApi.delete(metric);
MetabaseAnalytics.trackEvent("Data Model", "Retire Metric");
......@@ -193,10 +205,10 @@ export const onRetireMetric = createThunkAction("RETIRE_METRIC", function(metric
// SEGMENTS
export const GET_SEGMENT = "GET_SEGMENT";
export const CREATE_SEGMENT = "CREATE_SEGMENT";
export const UPDATE_SEGMENT = "UPDATE_SEGMENT";
export const DELETE_SEGMENT = "DELETE_SEGMENT";
export const GET_SEGMENT = "metabase/admin/datamodel/GET_SEGMENT";
export const CREATE_SEGMENT = "metabase/admin/datamodel/CREATE_SEGMENT";
export const UPDATE_SEGMENT = "metabase/admin/datamodel/UPDATE_SEGMENT";
export const DELETE_SEGMENT = "metabase/admin/datamodel/DELETE_SEGMENT";
export const getSegment = createAction(GET_SEGMENT, SegmentApi.get);
export const createSegment = createAction(CREATE_SEGMENT, SegmentApi.create);
......@@ -205,10 +217,10 @@ export const deleteSegment = createAction(DELETE_SEGMENT, SegmentApi.delete);
// METRICS
export const GET_METRIC = "GET_METRIC";
export const CREATE_METRIC = "CREATE_METRIC";
export const UPDATE_METRIC = "UPDATE_METRIC";
export const DELETE_METRIC = "DELETE_METRIC";
export const GET_METRIC = "metabase/admin/datamodel/GET_METRIC";
export const CREATE_METRIC = "metabase/admin/datamodel/CREATE_METRIC";
export const UPDATE_METRIC = "metabase/admin/datamodel/UPDATE_METRIC";
export const DELETE_METRIC = "metabase/admin/datamodel/DELETE_METRIC";
export const getMetric = createAction(GET_METRIC, MetricApi.get);
export const createMetric = createAction(CREATE_METRIC, MetricApi.create);
......@@ -217,8 +229,8 @@ export const deleteMetric = createAction(DELETE_METRIC, MetricApi.delete);
// SEGMENT DETAIL
export const LOAD_TABLE_METADATA = "LOAD_TABLE_METADATA";
export const UPDATE_PREVIEW_SUMMARY = "UPDATE_PREVIEW_SUMMARY";
export const LOAD_TABLE_METADATA = "metabase/admin/datamodel/LOAD_TABLE_METADATA";
export const UPDATE_PREVIEW_SUMMARY = "metabase/admin/datamodel/UPDATE_PREVIEW_SUMMARY";
export const loadTableMetadata = createAction(LOAD_TABLE_METADATA, loadTableAndForeignKeys);
export const updatePreviewSummary = createAction(UPDATE_PREVIEW_SUMMARY, async (query) => {
......@@ -228,7 +240,7 @@ export const updatePreviewSummary = createAction(UPDATE_PREVIEW_SUMMARY, async (
// REVISION HISTORY
export const FETCH_REVISIONS = "FETCH_REVISIONS";
export const FETCH_REVISIONS = "metabase/admin/datamodel/FETCH_REVISIONS";
export const fetchRevisions = createThunkAction(FETCH_REVISIONS, ({ entity, id }) =>
async (dispatch, getState) => {
......@@ -250,23 +262,23 @@ export const fetchRevisions = createThunkAction(FETCH_REVISIONS, ({ entity, id }
// reducers
const databases = handleActions({
["INITIALIZE_METADATA"]: { next: (state, { payload }) => payload.databases }
[INITIALIZE_METADATA]: { next: (state, { payload }) => payload.databases }
}, []);
const idfields = handleActions({
["FETCH_IDFIELDS"]: { next: (state, { payload }) => payload ? payload : state }
[FETCH_IDFIELDS]: { next: (state, { payload }) => payload ? payload : state }
}, []);
const editingDatabase = handleActions({
["INITIALIZE_METADATA"]: { next: (state, { payload }) => payload.database },
["SELECT_DATABASE"]: { next: (state, { payload }) => payload ? payload : state },
["RETIRE_SEGMENT"]: { next: (state, { payload }) => payload },
["RETIRE_METRIC"]: { next: (state, { payload }) => payload }
[INITIALIZE_METADATA]: { next: (state, { payload }) => payload.database },
[SELECT_DATABASE]: { next: (state, { payload }) => payload ? payload : state },
[RETIRE_SEGMENT]: { next: (state, { payload }) => payload },
[RETIRE_METRIC]: { next: (state, { payload }) => payload }
}, null);
const editingTable = handleActions({
["INITIALIZE_METADATA"]: { next: (state, { payload }) => payload.tableId || null },
["SELECT_TABLE"]: { next: (state, { payload }) => payload }
[INITIALIZE_METADATA]: { next: (state, { payload }) => payload.tableId || null },
[SELECT_TABLE]: { next: (state, { payload }) => payload }
}, null);
const segments = handleActions({
......
......@@ -3,12 +3,12 @@ import { createSelector } from 'reselect';
import { computeMetadataStrength } from "metabase/lib/schema_metadata";
const segmentsSelector = (state, props) => state.datamodel.segments;
const metricsSelector = (state, props) => state.datamodel.metrics;
const segmentsSelector = (state, props) => state.admin.datamodel.segments;
const metricsSelector = (state, props) => state.admin.datamodel.metrics;
const tableMetadataSelector = (state, props) => state.datamodel.tableMetadata;
const previewSummarySelector = (state, props) => state.datamodel.previewSummary;
const revisionObjectSelector = (state, props) => state.datamodel.revisionObject;
const tableMetadataSelector = (state, props) => state.admin.datamodel.tableMetadata;
const previewSummarySelector = (state, props) => state.admin.datamodel.previewSummary;
const revisionObjectSelector = (state, props) => state.admin.datamodel.revisionObject;
const idSelector = (state, props) => props.params.id == null ? null : parseInt(props.params.id);
const tableIdSelector = (state, props) => props.location.query.table == null ? null : parseInt(props.location.query.table);
......@@ -73,13 +73,14 @@ export const revisionHistorySelectors = createSelector(
);
export const getDatabases = (state, props) => state.datamodel.databases;
export const getDatabaseIdfields = (state, props) => state.datamodel.idfields;
export const getEditingTable = (state, props) => state.datamodel.editingTable;
export const getDatabases = (state, props) => state.admin.datamodel.databases;
export const getDatabaseIdfields = (state, props) => state.admin.datamodel.idfields;
export const getEditingTable = (state, props) => state.admin.datamodel.editingTable;
export const getEditingDatabase = (state, props) => state.admin.datamodel.editingDatabase;
export const getEditingDatabaseWithTableMetadataStrengths = createSelector(
state => state.datamodel.editingDatabase,
state => state.admin.datamodel.editingDatabase,
(database) => {
if (!database || !database.tables) {
return null;
......
......@@ -2,14 +2,14 @@
import { createSelector } from 'reselect';
import _ from "underscore";
export const getGroups = (state) => state.people.groups;
export const getGroup = (state) => state.people.group;
export const getModal = (state) => state.people.modal;
export const getMemberships = (state) => state.people.memberships;
export const getGroups = (state) => state.admin.people.groups;
export const getGroup = (state) => state.admin.people.group;
export const getModal = (state) => state.admin.people.modal;
export const getMemberships = (state) => state.admin.people.memberships;
export const getUsers = createSelector(
(state) => state.people.users,
(state) => state.people.memberships,
(state) => state.admin.people.users,
(state) => state.admin.people.memberships,
(users, memberships) =>
users && _.mapObject(users, user => ({
...user,
......
......@@ -34,7 +34,7 @@ export const loadGroups = createAction(LOAD_GROUPS, () => PermissionsApi.groups(
const LOAD_PERMISSIONS = "metabase/admin/permissions/LOAD_PERMISSIONS";
export const loadPermissions = createThunkAction(LOAD_PERMISSIONS, () =>
async (dispatch, getState) => {
const { load } = getState().permissions;
const { load } = getState().admin.permissions;
return load();
}
);
......@@ -56,7 +56,7 @@ const SAVE_PERMISSIONS = "metabase/admin/permissions/SAVE_PERMISSIONS";
export const savePermissions = createThunkAction(SAVE_PERMISSIONS, () =>
async (dispatch, getState) => {
MetabaseAnalytics.trackEvent("Permissions", "save");
const { permissions, revision, save } = getState().permissions;
const { permissions, revision, save } = getState().admin.permissions;
let result = await save({
revision: revision,
groups: permissions
......
......@@ -27,14 +27,14 @@ import {
diffPermissions,
} from "metabase/lib/permissions";
const getPermissions = (state) => state.permissions.permissions;
const getOriginalPermissions = (state) => state.permissions.originalPermissions;
const getPermissions = (state) => state.admin.permissions.permissions;
const getOriginalPermissions = (state) => state.admin.permissions.originalPermissions;
const getDatabaseId = (state, props) => props.params.databaseId ? parseInt(props.params.databaseId) : null
const getSchemaName = (state, props) => props.params.schemaName
const getMetadata = createSelector(
[(state) => state.permissions.databases],
[(state) => state.admin.permissions.databases],
(databases) => databases && new Metadata(databases)
);
......@@ -53,7 +53,7 @@ function getTooltipForGroup(group) {
}
export const getGroups = createSelector(
(state) => state.permissions.groups,
(state) => state.admin.permissions.groups,
(groups) => {
let orderedGroups = groups ? [...groups] : [];
for (let groupFilter of SPECIAL_GROUP_FILTERS) {
......@@ -75,7 +75,7 @@ export const getIsDirty = createSelector(
JSON.stringify(permissions) !== JSON.stringify(originalPermissions)
)
export const getSaveError = (state) => state.permissions.saveError;
export const getSaveError = (state) => state.admin.permissions.saveError;
// these are all the permission levels ordered by level of access
......@@ -418,7 +418,7 @@ export const getDatabasesPermissionsGrid = createSelector(
}
);
const getCollections = (state) => state.permissions.collections;
const getCollections = (state) => state.admin.permissions.collections;
const getCollectionPermission = (permissions, groupId, { collectionId }) =>
getIn(permissions, [groupId, collectionId])
......
......@@ -31,12 +31,19 @@ export default class SettingsEmailForm extends Component {
componentWillMount() {
// this gives us an opportunity to load up our formData with any existing values for elements
this.updateFormData(this.props);
}
componentWillReceiveProps(nextProps) {
this.updateFormData(nextProps);
}
updateFormData(props) {
let formData = {};
this.props.elements.forEach(function(element) {
for (const element of props.elements) {
formData[element.key] = element.value;
});
this.setState({formData});
}
this.setState({ formData });
}
componentDidMount() {
......
......@@ -45,6 +45,9 @@ export default class SettingsSetting extends Component {
{ errorMessage &&
<div className="text-error text-bold pt1">{errorMessage}</div>
}
{ setting.warning &&
<div className="text-gold text-bold pt1">{setting.warning}</div>
}
</li>
);
}
......
......@@ -95,7 +95,7 @@ const SECTIONS = [
display_name: "SMTP Security",
description: null,
type: "radio",
options: { none: "None", ssl: "SSL", tls: "TLS" },
options: { none: "None", ssl: "SSL", tls: "TLS", starttls: "STARTTLS" },
defaultValue: 'none'
},
{
......@@ -270,7 +270,15 @@ for (const section of SECTIONS) {
section.slug = slugify(section.name);
}
export const getSettings = state => state.settings.settings;
export const getSettings = createSelector(
state => state.settings.settings,
state => state.admin.settings.warnings,
(settings, warnings) =>
settings.map(setting => warnings[setting.key] ?
{ ...setting, warning: warnings[setting.key] } :
setting
)
)
export const getSettingValues = createSelector(
getSettings,
......
import { createThunkAction } from "metabase/lib/redux";
import { createThunkAction, handleActions, combineReducers } from "metabase/lib/redux";
import { SettingsApi, EmailApi, SlackApi } from "metabase/services";
import { refreshSiteSettings } from "metabase/redux/settings";
// initializeSettings
export const initializeSettings = createThunkAction("INITIALIZE_SETTINGS", function() {
// ACITON TYPES AND ACTION CREATORS
export const INITIALIZE_SETTINGS = "metabase/admin/settings/INITIALIZE_SETTINGS";
export const initializeSettings = createThunkAction(INITIALIZE_SETTINGS, function() {
return async function(dispatch, getState) {
try {
await dispatch(refreshSiteSettings());
......@@ -17,8 +19,8 @@ export const initializeSettings = createThunkAction("INITIALIZE_SETTINGS", funct
};
});
// updateSetting
export const updateSetting = createThunkAction("UPDATE_SETTING", function(setting) {
export const UPDATE_SETTING = "metabase/admin/settings/UPDATE_SETTING";
export const updateSetting = createThunkAction(UPDATE_SETTING, function(setting) {
return async function(dispatch, getState) {
try {
await SettingsApi.put(setting);
......@@ -30,12 +32,13 @@ export const updateSetting = createThunkAction("UPDATE_SETTING", function(settin
};
});
// updateEmailSettings
export const updateEmailSettings = createThunkAction("UPDATE_EMAIL_SETTINGS", function(settings) {
export const UPDATE_EMAIL_SETTINGS = "metabase/admin/settings/UPDATE_EMAIL_SETTINGS";
export const updateEmailSettings = createThunkAction(UPDATE_EMAIL_SETTINGS, function(settings) {
return async function(dispatch, getState) {
try {
await EmailApi.updateSettings(settings);
const result = await EmailApi.updateSettings(settings);
await dispatch(refreshSiteSettings());
return result;
} catch(error) {
console.log("error updating email settings", settings, error);
throw error;
......@@ -43,8 +46,8 @@ export const updateEmailSettings = createThunkAction("UPDATE_EMAIL_SETTINGS", fu
};
});
// sendTestEmail
export const sendTestEmail = createThunkAction("SEND_TEST_EMAIL", function() {
export const SEND_TEST_EMAIL = "metabase/admin/settings/SEND_TEST_EMAIL";
export const sendTestEmail = createThunkAction(SEND_TEST_EMAIL, function() {
return async function(dispatch, getState) {
try {
await EmailApi.sendTest();
......@@ -55,8 +58,8 @@ export const sendTestEmail = createThunkAction("SEND_TEST_EMAIL", function() {
};
});
// updateSlackSettings
export const updateSlackSettings = createThunkAction("UPDATE_SLACK_SETTINGS", function(settings) {
export const UPDATE_SLACK_SETTINGS = "metabase/admin/settings/UPDATE_SLACK_SETTINGS";
export const updateSlackSettings = createThunkAction(UPDATE_SLACK_SETTINGS, function(settings) {
return async function(dispatch, getState) {
try {
await SlackApi.updateSettings(settings);
......@@ -68,8 +71,19 @@ export const updateSlackSettings = createThunkAction("UPDATE_SLACK_SETTINGS", fu
};
}, {});
export const reloadSettings = createThunkAction("RELOAD_SETTINGS", function() {
export const RELOAD_SETTINGS = "metabase/admin/settings/RELOAD_SETTINGS";
export const reloadSettings = createThunkAction(RELOAD_SETTINGS, function() {
return async function(dispatch, getState) {
await dispatch(refreshSiteSettings());
}
});
// REDUCERS
export const warnings = handleActions({
[UPDATE_EMAIL_SETTINGS]: { next: (state, { payload }) => payload["with-corrections"] }
}, {});
export default combineReducers({
warnings
});
......@@ -7,7 +7,7 @@ import { combineReducers } from 'redux';
import commonReducers from "./reducers-common";
/* admin */
import adminReducers from "./reducers-admin";
import admin from "metabase/admin/admin";
/* setup */
import * as setup from "metabase/setup/reducers";
......@@ -45,6 +45,5 @@ export default {
reference,
setup: combineReducers(setup),
user: combineReducers(user),
...adminReducers
admin
};
(ns metabase.api.email
"/api/email endpoints"
(:require [clojure.tools.logging :as log]
[clojure.set :as set]
[compojure.core :refer [GET PUT DELETE POST]]
(:require [clojure
[data :as data]
[set :as set]
[string :as string]]
[clojure.tools.logging :as log]
[compojure.core :refer [POST PUT]]
[metabase
[config :as config]
[email :as email]]
[metabase.api.common :refer :all]
[metabase.config :as config]
[metabase.email :as email]
[metabase.models.setting :as setting]
[metabase.util.schema :as su]))
......@@ -51,6 +55,16 @@
#".*"
{:message "Sorry, something went wrong. Please try again."}))))
(defn humanize-email-corrections
"formats warnings when security settings are autocorrected"
[corrections]
(into {}
(mapv (fn [[k v]]
[k (format "%s was autocorrected to %s"
(name (mb-to-smtp-settings k))
(string/upper-case v))])
corrections)))
(defendpoint PUT "/"
"Update multiple `Settings` values. You must be a superuser to do this."
[:as {settings :body}]
......@@ -63,10 +77,15 @@
;; in normal conditions, validate connection
(email/test-smtp-connection smtp-settings)
;; for unit testing just respond with a success message
{:error :SUCCESS})]
{:error :SUCCESS})
tested-settings (merge settings (select-keys response [:port :security]))
[_ corrections _] (data/diff settings tested-settings)
properly-named-corrections (set/rename-keys corrections (set/map-invert mb-to-smtp-settings))
corrected-settings (merge email-settings properly-named-corrections)]
(if (= :SUCCESS (:error response))
;; test was good, save our settings
(setting/set-many! email-settings)
(assoc (setting/set-many! corrected-settings)
:with-corrections (humanize-email-corrections properly-named-corrections))
;; test failed, return response message
{:status 500
:body (humanize-error-messages response)})))
......
......@@ -15,11 +15,11 @@
(defsetting email-smtp-password "SMTP password.")
(defsetting email-smtp-port "The port your SMTP server uses for outgoing emails.")
(defsetting email-smtp-security
"SMTP secure connection protocol. (tls, ssl, or none)"
"SMTP secure connection protocol. (tls, ssl, starttls, or none)"
:default "none"
:setter (fn [new-value]
(when-not (nil? new-value)
(assert (contains? #{"tls" "ssl" "none"} new-value)))
(assert (contains? #{"tls" "ssl" "none" "starttls"} new-value)))
(setting/set-string! :email-smtp-security new-value)))
;; ## PUBLIC INTERFACE
......@@ -38,6 +38,8 @@
(merge m (case (keyword ssl-setting)
:tls {:tls true}
:ssl {:ssl true}
:starttls {:starttls.enable true
:starttls.required true}
{})))
(defn- smtp-settings []
......@@ -85,27 +87,19 @@
{:error :ERROR
:message (.getMessage e)})))
(defn test-smtp-connection
"Test the connection to an SMTP server to determine if we can send emails.
Takes in a dictionary of properties such as:
{:host \"localhost\"
:port 587
:user \"bigbird\"
:pass \"luckyme\"
:sender \"foo@mycompany.com\"
:security \"tls\"}"
(defn- run-smtp-test
"tests an SMTP configuration by attempting to connect and authenticate
if an authenticated method is passed in :security."
[{:keys [host port user pass sender security] :as details}]
{:pre [(string? host)
(integer? port)]}
(try
(let [ssl? (= security "ssl")
proto (if ssl? "smtps" "smtp")
(let [ssl? (= security "ssl")
proto (if ssl? "smtps" "smtp")
details (-> details
(assoc :proto proto
:connectiontimeout "1000"
:timeout "1000")
:timeout "4000")
(add-ssl-settings security))
session (doto (Session/getInstance (make-props sender details))
(.setDebug false))]
......@@ -117,3 +111,42 @@
(log/error "Error testing SMTP connection:" (.getMessage e))
{:error :ERROR
:message (.getMessage e)})))
(def ^:private email-security-order ["tls" "starttls" "ssl"])
(defn- guess-smtp-security
"Attempts to use each of the security methods in security order with the same set of credentials.
This is used only when the initial connection attempt fails, so it won't overwrite a functioning
configuration. If this uses something other than the provided method, a warning gets printed on
the config page"
[details]
(loop [[security-type & more-to-try] email-security-order] ;; make sure this is not lazy, or chunking
(when security-type ;; can cause some servers to block requests
(let [test-result (run-smtp-test (assoc details :security security-type))]
(if (not= :ERROR (:error test-result))
(assoc test-result :security security-type)
(do
(Thread/sleep 500) ;; try not to get banned from outlook.com
(recur more-to-try)))))))
(defn test-smtp-connection
"Test the connection to an SMTP server to determine if we can send emails.
Takes in a dictionary of properties such as:
{:host \"localhost\"
:port 587
:user \"bigbird\"
:pass \"luckyme\"
:sender \"foo@mycompany.com\"
:security \"tls\"}"
[details]
(let [inital-attempt (run-smtp-test details)
it-worked? (= :SUCCESS (:error inital-attempt))
attempted-fix (if (not it-worked?)
(guess-smtp-security details))
we-fixed-it? (= :SUCCESS (:error attempted-fix))]
(if it-worked?
inital-attempt
(if we-fixed-it?
attempted-fix
inital-attempt))))
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment