Просмотр исходного кода

Merge pull request #2752 from weseek/imprv/can-update-SES-setting-in-the-mail-setting-form

Imprv/can update ses setting in the mail setting form
Yuki Takei 5 лет назад
Родитель
Сommit
8b7f49270b

+ 8 - 7
resource/locales/en_US/admin/admin.json

@@ -27,20 +27,21 @@
     "attach_enable": "You can attach files other than image files if you enable this option.",
     "update": "Update",
     "mail_settings": "E-mail Settings",
-    "smtp_used": "If you have SMTP settings, it will be used.",
-    "smtp_but_aws": "If you do not have SMTP settings but AWS settings,  e-mails will be sent by SES.",
-    "neihter_of": "If neither is selected, then no email will be sent.",
+    "mailer_is_not_set_up": "E-mail setting is not set up.",
     "from_e-mail_address": "From e-mail address",
+    "transmission_method":"Transmission Method",
+    "smtp_label":"SMTP",
+    "ses_label":"SES(AWS)",
+    "send_test_email": "Send a test-email",
+    "success_to_send_test_email": "Success to send a test-email",
     "smtp_settings": "SMTP settings",
     "host": "Host",
     "port": "Port",
     "user": "User",
-    "initialize_mail_settings": "initialize e-mail settings",
-    "initialize_mail_modal_header": "Initialize e-mail settings",
-    "confirm_to_initialize_mail_settings": "You can't restore to the current settings. Are you sure you want to initialize e-mail settings?",
+    "ses_settings":"SES settings",
+    "test_connection": "Test connection to mail",
     "aws_settings": "AWS settings",
     "aws_access": "This is for AWS settings. If you complete AWS settings, file upload function, profile picture function etc will be enabled.",
-    "no_smtp_setting": "If you do not have SMTP settings, e-mails will be sent via SES. You need to verify from e-mail address and production settings.",
     "change_setting": "Caution:if you change this setting not completed, you will not be able to access files you have uploaded so far.",
     "region": "Region",
     "bucket_name": "Bucket name",

+ 2 - 1
resource/locales/en_US/translation.json

@@ -752,6 +752,7 @@
   },
   "validation":{
     "aws_region": "For the region, enter the AWS region name. ex):us-east-1",
-    "aws_custom_endpoint":"For the custom endpoint, specify the URL that starts with http(s)://. Also, the trailing slash is not required."
+    "aws_custom_endpoint":"For the custom endpoint, specify the URL that starts with http(s)://. Also, the trailing slash is not required.",
+    "failed_to_send_a_test_email":"Failed to send a test email using SMTP. Please check your settings."
   }
 }

+ 8 - 7
resource/locales/ja_JP/admin/admin.json

@@ -27,20 +27,21 @@
     "attach_enable": "許可をしている場合、画像以外のファイルをページに添付可能になります。",
     "update": "更新",
     "mail_settings": "メールの設定",
-    "smtp_used": "SMTPの設定がされている場合、それが利用されます。",
-    "smtp_but_aws": "SMTP設定がなく、AWSの設定がある場合、SESでの送信を試みます。",
-    "neihter_of": "どちらの設定もない場合、メールは送信されません。",
+    "mailer_is_not_set_up": "メール設定がセットアップされていません。",
     "from_e-mail_address": "Fromアドレス",
+    "transmission_method":"送信方法",
+    "smtp_label":"SMTP",
+    "ses_label":"SES(AWS)",
+    "send_test_email": "テストメールを送信",
+    "success_to_send_test_email": "テストメールを送信しました。",
     "smtp_settings": "SMTP設定",
     "host": "ホスト",
     "port": "ポート",
     "user": "ユーザー",
-    "initialize_mail_settings": "設定を初期化",
-    "initialize_mail_modal_header": "メール設定の初期化",
-    "confirm_to_initialize_mail_settings": "一度初期化した設定は戻せません。本当に初期化しますか?",
+    "ses_settings":"SES設定",
+    "test_connection": "接続テスト",
     "aws_settings": "AWS設定",
     "aws_access": "AWS にアクセスするための設定を行います。AWS の設定を完了させると、ファイルアップロード機能、プロフィール写真機能などが有効になります。",
-    "no_smtp_setting": "また、SMTP の設定が無い場合、SES を利用したメール送信が行われます。FromメールアドレスのVerify、プロダクション利用設定をする必要があります。",
     "change_setting": "この設定を途中で変更すると、これまでにアップロードしたファイル等へのアクセスができなくなりますのでご注意下さい。",
     "region": "リージョン",
     "bucket_name": "バケット名",

+ 2 - 1
resource/locales/ja_JP/translation.json

@@ -745,6 +745,7 @@
   },
   "validation":{
     "aws_region": "リージョンには、AWSリージョン名を入力してください。例: ap-northeast-1",
-    "aws_custom_endpoint": "カスタムエンドポイントは、http(s)://で始まるURLを指定してください。また、末尾の/は不要です。"
+    "aws_custom_endpoint": "カスタムエンドポイントは、http(s)://で始まるURLを指定してください。また、末尾の/は不要です。",
+    "failed_to_send_a_test_email":"SMTPを利用したテストメール送信に失敗しました。設定をみなおしてください。"
   }
 }

+ 10 - 8
resource/locales/zh_CN/admin/admin.json

@@ -27,20 +27,22 @@
 		"attach_enable": "如果启用此选项,则可以附加图像文件以外的文件。",
 		"update": "更新",
 		"mail_settings": "邮件设置",
-		"smtp_used": "如果您有SMTP设置,将使用它。",
-		"smtp_but_aws": "如果您没有SMTP设置,但有AWS设置,则电子邮件将由SES发送。",
-		"neihter_of": "如果两者都未选中,则不会发送电子邮件。",
+    "mailer_is_not_set_up": "E-mail setting is not set up.",
+    "transmission_method":"Transmission Method",
+    "smtp_label":"SMTP",
+    "ses_label":"SES(AWS)",
 		"from_e-mail_address": "From e-mail address",
-		"smtp_settings": "SMTP 设置",
+    "send_test_email": "Send a test-email",
+    "success_to_send_test_email": "Success to send a test-email",
+    "smtp_settings": "SMTP 设置",
 		"host": "服务器",
 		"port": "端口号",
 		"user": "用户名",
-    "initialize_mail_settings": "重置邮件设置",
-    "initialize_mail_modal_header": "重置邮件设置",
-    "confirm_to_initialize_mail_settings": "当前设置将被清空且不可恢复。确认重置?",
+    "ses_settings":"SES设置",
+    "test_connection": "Test connection to mail",
 		"aws_settings": "AWS设置",
 		"aws_access": "这是用于AWS设置的。如果您完成了AWS设置,文件上传功能,个人资料图片功能等将被启用。",
-		"no_smtp_setting": "如果您没有SMTP设置,电子邮件将通过SES发送。您需要从电子邮件地址和生产设置进行验证。",
+		"": "如果您没有SMTP设置,电子邮件将通过SES发送。您需要从电子邮件地址和生产设置进行验证。",
 		"change_setting": "注意:如果你更改此设置未完成,您将无法访问迄今为止上传的文件。",
 		"region": "Region",
 		"bucket_name": "Bucket name",

+ 2 - 1
resource/locales/zh_CN/translation.json

@@ -756,6 +756,7 @@
   },
   "validation":{
     "aws_region": "关于地区,请输入AWS地区名,例如:ap-east-1",
-    "aws_custom_endpoint": "关于自定义端点,请指定以http(s)://开头的URL,链接末尾不需要添加“/”"
+    "aws_custom_endpoint": "关于自定义端点,请指定以http(s)://开头的URL,链接末尾不需要添加“/”",
+    "failed_to_send_a_test_email":"SMTP方式测试邮件发送失败,请检查相关设定。"
   }
 }

+ 0 - 3
src/client/js/components/Admin/App/AwsSetting.jsx

@@ -41,9 +41,6 @@ class AwsSetting extends React.Component {
         <p className="card well">
           {t('admin:app_setting.aws_access')}
           <br />
-          {t('admin:app_setting.no_smtp_setting')}
-          <br />
-          <br />
           <span className="text-danger">
             <i className="ti-unlink"></i>
             {t('admin:app_setting.change_setting')}

+ 70 - 169
src/client/js/components/Admin/App/MailSetting.jsx

@@ -1,204 +1,105 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
-import loggerFactory from '@alias/logger';
 
-import { Modal, ModalHeader, ModalBody } from 'reactstrap';
-import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
+import { withUnstatedContainers } from '../../UnstatedUtils';
 
 import AppContainer from '../../../services/AppContainer';
 import AdminAppContainer from '../../../services/AdminAppContainer';
+import SmtpSetting from './SmtpSetting';
+import SesSetting from './SesSetting';
 
-const logger = loggerFactory('growi:appSettings');
-
-class MailSetting extends React.Component {
 
-  constructor(props) {
-    super(props);
+function MailSetting(props) {
+  const { t, adminAppContainer } = props;
 
-    this.state = {
-      isInitializeValueModalOpen: false,
-    };
+  const transmissionMethods = ['smtp', 'ses'];
 
-    this.emailInput = React.createRef();
-    this.hostInput = React.createRef();
-    this.portInput = React.createRef();
-    this.userInput = React.createRef();
-    this.passwordInput = React.createRef();
-
-    this.openInitializeValueModal = this.openInitializeValueModal.bind(this);
-    this.closeInitializeValueModal = this.closeInitializeValueModal.bind(this);
-    this.submitFromAdressHandler = this.submitFromAdressHandler.bind(this);
-    this.submitHandler = this.submitHandler.bind(this);
-    this.initialize = this.initialize.bind(this);
-  }
-
-  openInitializeValueModal() {
-    this.setState({ isInitializeValueModalOpen: true });
-  }
-
-  closeInitializeValueModal() {
-    this.setState({ isInitializeValueModalOpen: false });
-  }
-
-  async submitHandler() {
-    const { t, adminAppContainer } = this.props;
+  async function submitHandler() {
+    const { t } = props;
 
     try {
       await adminAppContainer.updateMailSettingHandler();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:app_setting.mail_settings') }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:app_setting.ses_settings') }));
     }
     catch (err) {
       toastError(err);
-      logger.error(err);
     }
   }
 
-  async submitFromAdressHandler() {
-    const { t, adminAppContainer } = this.props;
-
+  async function sendTestEmailHandler() {
+    const { adminAppContainer } = props;
     try {
-      await adminAppContainer.updateFromAdressHandler();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:app_setting.mail_settings') }));
+      await adminAppContainer.sendTestEmail();
+      toastSuccess(t('admin:app_setting.success_to_send_test_email'));
     }
     catch (err) {
       toastError(err);
-      logger.error(err);
     }
   }
 
-  async initialize() {
-    const { t, adminAppContainer } = this.props;
 
-    try {
-      const mailSettingParams = await adminAppContainer.initializeMailSettingHandler();
-      toastSuccess(t('toaster.initialize_successed', { target: t('admin:app_setting.smtp_settings') }));
-      // convert values to '' if value is null for overwriting values of inputs with refs
-      this.hostInput.current.value = mailSettingParams.smtpHost || '';
-      this.portInput.current.value = mailSettingParams.smtpPort || '';
-      this.userInput.current.value = mailSettingParams.smtpUser || '';
-      this.passwordInput.current.value = mailSettingParams.smtpPassword || '';
-      this.closeInitializeValueModal();
-    }
-    catch (err) {
-      toastError(err);
-      logger.error(err);
-    }
-  }
-
-  render() {
-    const { t, adminAppContainer } = this.props;
-
-    return (
-      <React.Fragment>
-        <p className="card well">{t('admin:app_setting.smtp_used')} {t('admin:app_setting.smtp_but_aws')}<br />{t('admin:app_setting.neihter_of')}</p>
-        <div className="row form-group mb-5">
-          <label className="col-md-3 col-form-label text-left">{t('admin:app_setting.from_e-mail_address')}</label>
-          <div className="col-md-6">
-            <input
-              className="form-control"
-              type="text"
-              ref={this.emailInput}
-              placeholder={`${t('eg')} mail@growi.org`}
-              defaultValue={adminAppContainer.state.fromAddress || ''}
-              onChange={(e) => { adminAppContainer.changeFromAddress(e.target.value) }}
-            />
-          </div>
+  return (
+    <React.Fragment>
+      {!adminAppContainer.state.isMailerSetup && (
+        <div className="alert alert-danger"><i className="icon-exclamation"></i> {t('admin:app_setting.mailer_is_not_set_up')}</div>
+      )}
+      <div className="row form-group mb-5">
+        <label className="col-md-3 col-form-label text-right">{t('admin:app_setting.from_e-mail_address')}</label>
+        <div className="col-md-6">
+          <input
+            className="form-control"
+            type="text"
+            placeholder={`${t('eg')} mail@growi.org`}
+            defaultValue={adminAppContainer.state.fromAddress || ''}
+            onChange={(e) => { adminAppContainer.changeFromAddress(e.target.value) }}
+          />
         </div>
-        <div className="row my-3">
-          <div className="mx-auto">
-            <button type="button" className="btn btn-primary" onClick={this.submitFromAdressHandler}>{ t('Update') }</button>
-          </div>
+      </div>
+
+      <div className="row form-group mb-5">
+        <label className="text-left text-md-right col-md-3 col-form-label">
+          {t('admin:app_setting.transmission_method')}
+        </label>
+        <div className="col-md-6">
+          {transmissionMethods.map((method) => {
+              return (
+                <div key={method} className="custom-control custom-radio custom-control-inline">
+                  <input
+                    type="radio"
+                    className="custom-control-input"
+                    name="transmission-method"
+                    id={`transmission-nethod-radio-${method}`}
+                    checked={adminAppContainer.state.transmissionMethod === method}
+                    onChange={(e) => {
+                    adminAppContainer.changeTransmissionMethod(method);
+                  }}
+                  />
+                  <label className="custom-control-label" htmlFor={`transmission-nethod-radio-${method}`}>{t(`admin:app_setting.${method}_label`)}</label>
+                </div>
+              );
+            })}
         </div>
-        <div id="mail-smtp" className="tab-pane active mt-5">
-          <div className="row form-group mb-5">
-            <label className="col-md-3 col-form-label text-left">{t('admin:app_setting.smtp_settings')}</label>
-            <div className="col-md-4">
-              <label>{t('admin:app_setting.host')}</label>
-              <input
-                className="form-control"
-                type="text"
-                ref={this.hostInput}
-                defaultValue={adminAppContainer.state.smtpHost || ''}
-                onChange={(e) => { adminAppContainer.changeSmtpHost(e.target.value) }}
-              />
-            </div>
-            <div className="col-md-2">
-              <label>{t('admin:app_setting.port')}</label>
-              <input
-                className="form-control"
-                ref={this.portInput}
-                defaultValue={adminAppContainer.state.smtpPort || ''}
-                onChange={(e) => { adminAppContainer.changeSmtpPort(e.target.value) }}
-              />
-            </div>
-          </div>
-
-          <div className="row form-group mb-5">
-            <div className="col-md-3 offset-md-3">
-              <label>{t('admin:app_setting.user')}</label>
-              <input
-                className="form-control"
-                type="text"
-                ref={this.userInput}
-                defaultValue={adminAppContainer.state.smtpUser || ''}
-                onChange={(e) => { adminAppContainer.changeSmtpUser(e.target.value) }}
-              />
-            </div>
-            <div className="col-md-3">
-              <label>{t('Password')}</label>
-              <input
-                className="form-control"
-                type="password"
-                ref={this.passwordInput}
-                defaultValue={adminAppContainer.state.smtpPassword || ''}
-                onChange={(e) => { adminAppContainer.changeSmtpPassword(e.target.value) }}
-              />
-            </div>
-          </div>
-
-          <div className="row my-3">
-            <div className="offset-5">
-              <button type="button" className="btn btn-primary" onClick={this.submitHandler} disabled={adminAppContainer.state.retrieveError != null}>
-                { t('Update') }
-              </button>
-            </div>
-            <div className="offset-1">
-              <button
-                type="button"
-                className="btn btn-secondary"
-                onClick={this.openInitializeValueModal}
-                disabled={adminAppContainer.state.retrieveError != null}
-              >
-                {t('admin:app_setting.initialize_mail_settings')}
-              </button>
-            </div>
-          </div>
+      </div>
+
+      {adminAppContainer.state.transmissionMethod === 'smtp' && <SmtpSetting />}
+      {adminAppContainer.state.transmissionMethod === 'ses' && <SesSetting />}
+
+      <div className="row my-3">
+        <div className="mx-auto">
+          <button type="button" className="btn btn-primary" onClick={submitHandler} disabled={adminAppContainer.state.retrieveError != null}>
+            { t('Update') }
+          </button>
+          {adminAppContainer.state.transmissionMethod === 'smtp' && (
+          <button type="button" className="btn btn-secondary ml-4" onClick={sendTestEmailHandler}>
+            {t('admin:app_setting.send_test_email')}
+          </button>
+          )}
         </div>
-
-
-        <Modal isOpen={this.state.isInitializeValueModalOpen} toggle={this.closeInitializeValueModal} className="initialize-mail-settings">
-          <ModalHeader tag="h4" toggle={this.closeInitializeValueModal} className="bg-danger text-light">
-            {t('admin:app_setting.initialize_mail_modal_header')}
-          </ModalHeader>
-          <ModalBody>
-            <div className="text-center mb-4">
-              {t('admin:app_setting.confirm_to_initialize_mail_settings')}
-            </div>
-            <div className="text-center my-2">
-              <button type="button" className="btn btn-outline-secondary mr-4" onClick={this.closeInitializeValueModal}>
-                {t('Cancel')}
-              </button>
-              <button type="button" className="btn btn-danger" onClick={this.initialize}>
-                {t('Reset')}
-              </button>
-            </div>
-          </ModalBody>
-        </Modal>
-      </React.Fragment>
-    );
-  }
+      </div>
+    </React.Fragment>
+  );
 
 }
 

+ 68 - 0
src/client/js/components/Admin/App/SesSetting.jsx

@@ -0,0 +1,68 @@
+
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
+import { withLoadingSppiner } from '../../SuspenseUtils';
+
+
+import AppContainer from '../../../services/AppContainer';
+import AdminAppContainer from '../../../services/AdminAppContainer';
+
+function SmtpSetting(props) {
+  const { adminAppContainer } = props;
+
+  return (
+    <React.Fragment>
+      <div id="mail-smtp" className="tab-pane active mt-5">
+
+        <div className="row form-group">
+          <label className="text-left text-md-right col-md-3 col-form-label">
+            Access key ID
+          </label>
+          <div className="col-md-6">
+            <input
+              className="form-control"
+              type="text"
+              defaultValue={adminAppContainer.state.sesAccessKeyId || ''}
+              onChange={(e) => {
+                adminAppContainer.changeSesAccessKeyId(e.target.value);
+              }}
+            />
+          </div>
+        </div>
+
+        <div className="row form-group">
+          <label className="text-left text-md-right col-md-3 col-form-label">
+            Secret access key
+          </label>
+          <div className="col-md-6">
+            <input
+              className="form-control"
+              type="text"
+              defaultValue={adminAppContainer.state.sesSecretAccessKey || ''}
+              onChange={(e) => {
+                adminAppContainer.changeSesSecretAccessKey(e.target.value);
+              }}
+            />
+          </div>
+        </div>
+      </div>
+
+    </React.Fragment>
+  );
+}
+
+/**
+ * Wrapper component for using unstated
+ */
+const SmtpSettingWrapper = withUnstatedContainers(withLoadingSppiner(SmtpSetting), [AppContainer, AdminAppContainer]);
+
+SmtpSetting.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminAppContainer: PropTypes.instanceOf(AdminAppContainer).isRequired,
+};
+
+export default withTranslation()(SmtpSettingWrapper);

+ 89 - 0
src/client/js/components/Admin/App/SmtpSetting.jsx

@@ -0,0 +1,89 @@
+
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
+import { withLoadingSppiner } from '../../SuspenseUtils';
+
+
+import AppContainer from '../../../services/AppContainer';
+import AdminAppContainer from '../../../services/AdminAppContainer';
+
+function SmtpSetting(props) {
+  const { adminAppContainer, t } = props;
+
+  return (
+    <React.Fragment>
+      <div id="mail-smtp" className="tab-pane active mt-5">
+        <div className="row form-group">
+          <label className="text-left text-md-right col-md-3 col-form-label">
+            {t('admin:app_setting.host')}
+          </label>
+          <div className="col-md-6">
+            <input
+              className="form-control"
+              type="text"
+              defaultValue={adminAppContainer.state.smtpHost || ''}
+              onChange={(e) => { adminAppContainer.changeSmtpHost(e.target.value) }}
+            />
+          </div>
+        </div>
+
+        <div className="row form-group">
+          <label className="text-left text-md-right col-md-3 col-form-label">
+            {t('admin:app_setting.port')}
+          </label>
+          <div className="col-md-6">
+            <input
+              className="form-control"
+              defaultValue={adminAppContainer.state.smtpPort || ''}
+              onChange={(e) => { adminAppContainer.changeSmtpPort(e.target.value) }}
+            />
+          </div>
+        </div>
+
+        <div className="row form-group">
+          <label className="text-left text-md-right col-md-3 col-form-label">
+            {t('admin:app_setting.user')}
+          </label>
+          <div className="col-md-6">
+            <input
+              className="form-control"
+              type="text"
+              defaultValue={adminAppContainer.state.smtpUser || ''}
+              onChange={(e) => { adminAppContainer.changeSmtpUser(e.target.value) }}
+            />
+          </div>
+        </div>
+
+        <div className="row form-group">
+          <label className="text-left text-md-right col-md-3 col-form-label">
+            {t('Password')}
+          </label>
+          <div className="col-md-6">
+            <input
+              className="form-control"
+              type="password"
+              defaultValue={adminAppContainer.state.smtpPassword || ''}
+              onChange={(e) => { adminAppContainer.changeSmtpPassword(e.target.value) }}
+            />
+          </div>
+        </div>
+      </div>
+    </React.Fragment>
+  );
+}
+
+/**
+ * Wrapper component for using unstated
+ */
+const SmtpSettingWrapper = withUnstatedContainers(withLoadingSppiner(SmtpSetting), [AppContainer, AdminAppContainer]);
+
+SmtpSetting.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminAppContainer: PropTypes.instanceOf(AdminAppContainer).isRequired,
+};
+
+export default withTranslation()(SmtpSettingWrapper);

+ 58 - 38
src/client/js/services/AdminAppContainer.js

@@ -23,11 +23,15 @@ export default class AdminAppContainer extends Container {
       siteUrl: '',
       envSiteUrl: '',
       isSetSiteUrl: true,
+      isMailerSetup: false,
       fromAddress: '',
+      transmissionMethod: '',
       smtpHost: '',
       smtpPort: '',
       smtpUser: '',
       smtpPassword: '',
+      sesAccessKeyId: '',
+      sesSecretAccessKey: '',
       region: '',
       customEndpoint: '',
       bucket: '',
@@ -36,27 +40,6 @@ export default class AdminAppContainer extends Container {
       isEnabledPlugins: true,
     };
 
-    this.changeTitle = this.changeTitle.bind(this);
-    this.changeConfidential = this.changeConfidential.bind(this);
-    this.changeGlobalLang = this.changeGlobalLang.bind(this);
-    this.changeFileUpload = this.changeFileUpload.bind(this);
-    this.changeSiteUrl = this.changeSiteUrl.bind(this);
-    this.changeFromAddress = this.changeFromAddress.bind(this);
-    this.changeSmtpHost = this.changeSmtpHost.bind(this);
-    this.changeSmtpPort = this.changeSmtpPort.bind(this);
-    this.changeSmtpUser = this.changeSmtpUser.bind(this);
-    this.changeSmtpPassword = this.changeSmtpPassword.bind(this);
-    this.changeRegion = this.changeRegion.bind(this);
-    this.changeCustomEndpoint = this.changeCustomEndpoint.bind(this);
-    this.changeBucket = this.changeBucket.bind(this);
-    this.changeAccessKeyId = this.changeAccessKeyId.bind(this);
-    this.changeSecretAccessKey = this.changeSecretAccessKey.bind(this);
-    this.changeIsEnabledPlugins = this.changeIsEnabledPlugins.bind(this);
-    this.updateAppSettingHandler = this.updateAppSettingHandler.bind(this);
-    this.updateSiteUrlSettingHandler = this.updateSiteUrlSettingHandler.bind(this);
-    this.updateMailSettingHandler = this.updateMailSettingHandler.bind(this);
-    this.updateAwsSettingHandler = this.updateAwsSettingHandler.bind(this);
-    this.updatePluginSettingHandler = this.updatePluginSettingHandler.bind(this);
   }
 
   /**
@@ -81,11 +64,15 @@ export default class AdminAppContainer extends Container {
       siteUrl: appSettingsParams.siteUrl,
       envSiteUrl: appSettingsParams.envSiteUrl,
       isSetSiteUrl: !!appSettingsParams.siteUrl,
+      isMailerSetup: appSettingsParams.isMailerSetup,
       fromAddress: appSettingsParams.fromAddress,
+      transmissionMethod: appSettingsParams.transmissionMethod,
       smtpHost: appSettingsParams.smtpHost,
       smtpPort: appSettingsParams.smtpPort,
       smtpUser: appSettingsParams.smtpUser,
       smtpPassword: appSettingsParams.smtpPassword,
+      sesAccessKeyId: appSettingsParams.sesAccessKeyId,
+      sesSecretAccessKey: appSettingsParams.sesSecretAccessKey,
       region: appSettingsParams.region,
       customEndpoint: appSettingsParams.customEndpoint,
       bucket: appSettingsParams.bucket,
@@ -138,6 +125,13 @@ export default class AdminAppContainer extends Container {
     this.setState({ fromAddress });
   }
 
+  /**
+   * Change from transmission method
+   */
+  changeTransmissionMethod(transmissionMethod) {
+    this.setState({ transmissionMethod });
+  }
+
   /**
    * Change smtp host
    */
@@ -166,6 +160,20 @@ export default class AdminAppContainer extends Container {
     this.setState({ smtpPassword });
   }
 
+  /**
+   * Change sesAccessKeyId
+   */
+  changeSesAccessKeyId(sesAccessKeyId) {
+    this.setState({ sesAccessKeyId });
+  }
+
+  /**
+   * Change sesSecretAccessKey
+   */
+  changeSesSecretAccessKey(sesSecretAccessKey) {
+    this.setState({ sesSecretAccessKey });
+  }
+
   /**
    * Change region
    */
@@ -239,49 +247,61 @@ export default class AdminAppContainer extends Container {
   }
 
   /**
-   * Update from adress
+   * Update mail setting
    * @memberOf AdminAppContainer
    * @return {Array} Appearance
    */
-  async updateFromAdressHandler() {
-    const response = await this.appContainer.apiv3.put('/app-settings/from-address', {
-      fromAddress: this.state.fromAddress,
-    });
-    const { mailSettingParams } = response.data;
-    return mailSettingParams;
+  updateMailSettingHandler() {
+    if (this.state.transmissionMethod === 'smtp') {
+      return this.updateSmtpSetting();
+    }
+    return this.updateSesSetting();
   }
 
   /**
-   * Update mail setting
+   * Update smtp setting
    * @memberOf AdminAppContainer
    * @return {Array} Appearance
    */
-  async updateMailSettingHandler() {
-    const response = await this.appContainer.apiv3.put('/app-settings/mail-setting', {
+  async updateSmtpSetting() {
+    const response = await this.appContainer.apiv3.put('/app-settings/smtp-setting', {
       fromAddress: this.state.fromAddress,
+      transmissionMethod: this.state.transmissionMethod,
       smtpHost: this.state.smtpHost,
       smtpPort: this.state.smtpPort,
       smtpUser: this.state.smtpUser,
       smtpPassword: this.state.smtpPassword,
     });
     const { mailSettingParams } = response.data;
+    this.setState({ isMailerSetup: mailSettingParams.isMailerSetup });
     return mailSettingParams;
   }
 
   /**
-   * Initialize mail setting
+   * Update ses setting
    * @memberOf AdminAppContainer
    * @return {Array} Appearance
    */
-  async initializeMailSettingHandler() {
-    const response = await this.appContainer.apiv3.delete('/app-settings/mail-setting', {});
-    const {
-      mailSettingParams,
-    } = response.data;
-    this.setState(mailSettingParams);
+  async updateSesSetting() {
+    const response = await this.appContainer.apiv3.put('/app-settings/ses-setting', {
+      fromAddress: this.state.fromAddress,
+      transmissionMethod: this.state.transmissionMethod,
+      sesAccessKeyId: this.state.sesAccessKeyId,
+      sesSecretAccessKey: this.state.sesSecretAccessKey,
+    });
+    const { mailSettingParams } = response.data;
+    this.setState({ isMailerSetup: mailSettingParams.isMailerSetup });
     return mailSettingParams;
   }
 
+  /**
+   * send test e-mail
+   * @memberOf AdminAppContainer
+   */
+  async sendTestEmail() {
+    return this.appContainer.apiv3.post('/app-settings/smtp-test');
+  }
+
   /**
    * Update AWS setting
    * @memberOf AdminAppContainer

+ 68 - 0
src/migrations/20200828024025-copy-aws-setting.js

@@ -0,0 +1,68 @@
+require('module-alias/register');
+const logger = require('@alias/logger')('growi:migrate:remove-layout-setting');
+
+const mongoose = require('mongoose');
+const config = require('@root/config/migrate');
+
+const { getModelSafely } = require('@commons/util/mongoose-utils');
+
+module.exports = {
+  async up(db, client) {
+    logger.info('Apply migration');
+    mongoose.connect(config.mongoUri, config.mongodb.options);
+
+    const Config = getModelSafely('Config') || require('@server/models/config')();
+
+    const [accessKeyId, secretAccessKey] = await Promise.all([
+      Config.findOne({ key: 'aws:accessKeyId' }),
+      Config.findOne({ key: 'aws:secretAccessKey' }),
+    ]);
+
+    const request = [];
+
+    if (accessKeyId != null) {
+      if (accessKeyId.value != null) {
+        request.push({
+          insertOne: {
+            document: {
+              key: 'mail:sesAccessKeyId',
+              ns: 'crowi',
+              value: accessKeyId.value,
+            },
+          },
+        });
+      }
+    }
+
+    if (secretAccessKey != null) {
+      if (secretAccessKey.value != null) {
+        request.push({
+          insertOne: {
+            document: {
+              key: 'mail:sesSecretAccessKey',
+              ns: 'crowi',
+              value: secretAccessKey.value,
+            },
+          },
+        });
+      }
+    }
+
+    if (request.length > 0) {
+      await Config.bulkWrite(request);
+    }
+
+    logger.info('Migration has successfully applied');
+  },
+
+  async down(db, client) {
+    logger.info('Rollback migration');
+    mongoose.connect(config.mongoUri, config.mongodb.options);
+
+    const Config = getModelSafely('Config') || require('@server/models/config')();
+
+    await Config.deleteMany({ key: { $in: ['mail:sesAccessKeyId', 'mail:sesSecretAccessKey'] } });
+
+    logger.info('Migration has been successfully rollbacked');
+  },
+};

+ 50 - 0
src/migrations/20200901034313-update-mail-transmission.js

@@ -0,0 +1,50 @@
+require('module-alias/register');
+const logger = require('@alias/logger')('growi:migrate:update-mail-transmission');
+
+const mongoose = require('mongoose');
+const config = require('@root/config/migrate');
+
+const { getModelSafely } = require('@commons/util/mongoose-utils');
+
+module.exports = {
+  async up(db, client) {
+    logger.info('Apply migration');
+    mongoose.connect(config.mongoUri, config.mongodb.options);
+
+    const Config = getModelSafely('Config') || require('@server/models/config')();
+
+    const sesExist = await Config.findOne({
+      ns: 'crowi',
+      key: 'mail:sesAccessKeyId',
+    });
+
+    if (sesExist == null) {
+      return logger.info('Document does not exist, value of transmission method will be set smtp automatically.');
+    }
+    const value = (
+      sesExist.value != null ? 'ses' : 'smtp'
+    );
+    await Config.create({
+      ns: 'crowi',
+      key: 'mail:transmissionMethod',
+      value,
+    });
+    logger.info('Migration has successfully applied');
+
+  },
+
+  async down(db, client) {
+    logger.info('Rollback migration');
+    mongoose.connect(config.mongoUri, config.mongodb.options);
+
+    const Config = getModelSafely('Config') || require('@server/models/config')();
+
+    // remote 'mail:transmissionMethod'
+    await Config.findOneAndDelete({
+      ns: 'crowi',
+      key: 'mail:transmissionMethod',
+    });
+
+    logger.info('Migration has been successfully rollbacked');
+  },
+};

+ 108 - 87
src/server/routes/apiv3/app-settings.js

@@ -50,15 +50,18 @@ const ErrorV3 = require('../../models/vo/error-apiv3');
  *          envSiteUrl:
  *            type: string
  *            description: environment variable 'APP_SITE_URL'
- *      FromAddress:
+ *      MailSetting:
  *        description: MailSettingParams
  *        type: object
  *        properties:
  *          fromAddress:
  *            type: string
  *            description: e-mail address used as from address of mail which sent from GROWI app
- *      MailSettingParams:
- *        description: MailSettingParams
+ *          transmissionMethod:
+ *            type: string
+ *            description: transmission method
+ *      SmtpSettingParams:
+ *        description: SmtpSettingParams
  *        type: object
  *        properties:
  *          smtpHost:
@@ -73,6 +76,16 @@ const ErrorV3 = require('../../models/vo/error-apiv3');
  *          smtpPassword:
  *            type: string
  *            description: password of client's smtp server
+ *      SesSettingParams:
+ *        description: SesSettingParams
+ *        type: object
+ *        properties:
+ *          accessKeyId:
+ *            type: string
+ *            description: accesskey id for authentification of AWS
+ *          secretAccessKey:
+ *            type: string
+ *            description: secret key for authentification of AWS
  *      AwsSettingParams:
  *        description: AwsSettingParams
  *        type: object
@@ -119,20 +132,25 @@ module.exports = (crowi) => {
     siteUrlSetting: [
       body('siteUrl').trim().matches(/^(https?:\/\/[^/]+|)$/).isURL({ require_tld: false }),
     ],
-    fromAddress: [
+    mailSetting: [
       body('fromAddress').trim().if(value => value !== '').isEmail(),
+      body('transmissionMethod').isIn(['smtp', 'ses']),
     ],
-    mailSetting: [
+    smtpSetting: [
       body('smtpHost').trim(),
-      body('smtpPort').trim().isPort(),
+      body('smtpPort').trim().if(value => value !== '').isPort(),
       body('smtpUser').trim(),
       body('smtpPassword').trim(),
     ],
+    sesSetting: [
+      body('sesAccessKeyId').trim().if(value => value !== '').matches(/^[\da-zA-Z]+$/),
+      body('sesSecretAccessKey').trim(),
+    ],
     awsSetting: [
       body('region').trim().matches(/^[a-z]+-[a-z]+-\d+$/).withMessage((value, { req }) => req.t('validation.aws_region')),
       body('customEndpoint').trim().matches(/^(https?:\/\/[^/]+|)$/).withMessage((value, { req }) => req.t('validation.aws_custom_endpoint')),
       body('bucket').trim(),
-      body('accessKeyId').trim().matches(/^[\da-zA-Z]+$/),
+      body('accessKeyId').trim().if(value => value !== '').matches(/^[\da-zA-Z]+$/),
       body('secretAccessKey').trim(),
     ],
     pluginSetting: [
@@ -168,11 +186,15 @@ module.exports = (crowi) => {
       fileUpload: crowi.configManager.getConfig('crowi', 'app:fileUpload'),
       siteUrl: crowi.configManager.getConfig('crowi', 'app:siteUrl'),
       envSiteUrl: crowi.configManager.getConfigFromEnvVars('crowi', 'app:siteUrl'),
+      isMailerSetup: crowi.mailService.isMailerSetup,
       fromAddress: crowi.configManager.getConfig('crowi', 'mail:from'),
+      transmissionMethod: crowi.configManager.getConfig('crowi', 'mail:transmissionMethod'),
       smtpHost: crowi.configManager.getConfig('crowi', 'mail:smtpHost'),
       smtpPort: crowi.configManager.getConfig('crowi', 'mail:smtpPort'),
       smtpUser: crowi.configManager.getConfig('crowi', 'mail:smtpUser'),
       smtpPassword: crowi.configManager.getConfig('crowi', 'mail:smtpPassword'),
+      sesAccessKeyId: crowi.configManager.getConfig('crowi', 'mail:sesAccessKeyId'),
+      sesSecretAccessKey: crowi.configManager.getConfig('crowi', 'mail:sesSecretAccessKey'),
       region: crowi.configManager.getConfig('crowi', 'aws:region'),
       customEndpoint: crowi.configManager.getConfig('crowi', 'aws:customEndpoint'),
       bucket: crowi.configManager.getConfig('crowi', 'aws:bucket'),
@@ -297,21 +319,32 @@ module.exports = (crowi) => {
   /**
    * validate mail setting send test mail
    */
-  async function validateMailSetting(req) {
+  async function sendTestEmail(destinationAddress) {
+
     const { configManager, mailService } = crowi;
+
+    if (!mailService.isMailerSetup) {
+      throw Error('mailService is not setup');
+    }
+
     const fromAddress = configManager.getConfig('crowi', 'mail:from');
     if (fromAddress == null) {
       throw Error('fromAddress is not setup');
     }
 
+    const smtpHost = configManager.getConfig('crowi', 'mail:smtpHost');
+    const smtpPort = configManager.getConfig('crowi', 'mail:smtpPort');
+    const smtpUser = configManager.getConfig('crowi', 'mail:smtpUser');
+    const smtpPassword = configManager.getConfig('crowi', 'mail:smtpPassword');
+
     const option = {
-      host: req.body.smtpHost,
-      port: req.body.smtpPort,
+      host: smtpHost,
+      port: smtpPort,
     };
-    if (req.body.smtpUser && req.body.smtpPassword) {
+    if (smtpUser && smtpPassword) {
       option.auth = {
-        user: req.body.smtpUser,
-        pass: req.body.smtpPassword,
+        user: smtpUser,
+        pass: smtpPassword,
       };
     }
     if (option.port === 465) {
@@ -323,7 +356,7 @@ module.exports = (crowi) => {
 
     const mailOptions = {
       from: fromAddress,
-      to: req.user.email,
+      to: destinationAddress,
       subject: 'Wiki管理設定のアップデートによるメール通知',
       text: 'このメールは、WikiのSMTP設定のアップデートにより送信されています。',
     };
@@ -344,143 +377,134 @@ module.exports = (crowi) => {
     mailService.publishUpdatedMessage();
 
     return {
+      isMailerSetup: mailService.isMailerSetup,
+      fromAddress: configManager.getConfig('crowi', 'mail:from'),
       smtpHost: configManager.getConfig('crowi', 'mail:smtpHost'),
       smtpPort: configManager.getConfig('crowi', 'mail:smtpPort'),
       smtpUser: configManager.getConfig('crowi', 'mail:smtpUser'),
       smtpPassword: configManager.getConfig('crowi', 'mail:smtpPassword'),
+      sesAccessKeyId: configManager.getConfig('crowi', 'mail:sesAccessKeyId'),
+      sesSecretAccessKey: configManager.getConfig('crowi', 'mail:sesSecretAccessKey'),
     };
   };
 
   /**
    * @swagger
    *
-   *    /app-settings/from-address:
+   *    /app-settings/smtp-setting:
    *      put:
    *        tags: [AppSettings]
-   *        operationId: updateAppSettingFromAddress
-   *        summary: /app-settings/from-address
-   *        description: Update from address
+   *        operationId: updateAppSettingSmtpSetting
+   *        summary: /app-settings/smtp-setting
+   *        description: Update smtp setting
    *        requestBody:
    *          required: true
    *          content:
    *            application/json:
    *              schema:
-   *                $ref: '#/components/schemas/FromAddress'
+   *                $ref: '#/components/schemas/SmtpSettingParams'
    *        responses:
    *          200:
-   *            description: Succeeded to update from adress
+   *            description: Succeeded to update smtp setting
    *            content:
    *              application/json:
    *                schema:
-   *                  $ref: '#/components/schemas/FromAddress'
+   *                  $ref: '#/components/schemas/SmtpSettingParams'
    */
-  router.put('/from-address', loginRequiredStrictly, adminRequired, csrf, validator.fromAddress, apiV3FormValidator, async(req, res) => {
+  router.put('/smtp-setting', loginRequiredStrictly, adminRequired, csrf, validator.smtpSetting, apiV3FormValidator, async(req, res) => {
+    const requestMailSettingParams = {
+      'mail:from': req.body.fromAddress,
+      'mail:transmissionMethod': req.body.transmissionMethod,
+      'mail:smtpHost': req.body.smtpHost,
+      'mail:smtpPort': req.body.smtpPort,
+      'mail:smtpUser': req.body.smtpUser,
+      'mail:smtpPassword': req.body.smtpPassword,
+    };
 
     try {
-      const mailSettingParams = await updateMailSettinConfig({ 'mail:from': req.body.fromAddress });
-
+      const mailSettingParams = await updateMailSettinConfig(requestMailSettingParams);
       return res.apiv3({ mailSettingParams });
     }
     catch (err) {
-      const msg = 'Error occurred in updating from adress';
+      const msg = 'Error occurred in updating smtp setting';
       logger.error('Error', err);
-      return res.apiv3Err(new ErrorV3(msg, 'update-from-adress-failed'));
+      return res.apiv3Err(new ErrorV3(msg, 'update-smtpSetting-failed'));
     }
-
   });
 
   /**
    * @swagger
    *
-   *    /app-settings/mail-setting:
-   *      put:
+   *    /app-settings/smtp-test:
+   *      post:
    *        tags: [AppSettings]
-   *        operationId: updateAppSettingMailSetting
-   *        summary: /app-settings/mail-setting
-   *        description: Update mail setting
-   *        requestBody:
-   *          required: true
-   *          content:
-   *            application/json:
-   *              schema:
-   *                $ref: '#/components/schemas/MailSettingParams'
+   *        operationId: postSmtpTest
+   *        summary: /app-settings/smtp-setting
+   *        description: Send test mail for smtp
    *        responses:
    *          200:
-   *            description: Succeeded to update mail setting
-   *            content:
-   *              application/json:
-   *                schema:
-   *                  $ref: '#/components/schemas/MailSettingParams'
+   *            description: Succeeded to send test mail for smtp
    */
-  router.put('/mail-setting', loginRequiredStrictly, adminRequired, csrf, validator.mailSetting, apiV3FormValidator, async(req, res) => {
+  router.post('/smtp-test', loginRequiredStrictly, adminRequired, async(req, res) => {
     try {
-      await validateMailSetting(req);
+      await sendTestEmail(req.user.email);
+      return res.apiv3({});
     }
     catch (err) {
-      const msg = 'SMTPを利用したテストメール送信に失敗しました。設定をみなおしてください。';
+      const msg = req.t('validation.failed_to_send_a_test_email');
       logger.error('Error', err);
       debug('Error validate mail setting: ', err);
-      return res.apiv3Err(new ErrorV3(msg, 'update-mailSetting-failed'));
-    }
-
-
-    const requestMailSettingParams = {
-      'mail:smtpHost': req.body.smtpHost,
-      'mail:smtpPort': req.body.smtpPort,
-      'mail:smtpUser': req.body.smtpUser,
-      'mail:smtpPassword': req.body.smtpPassword,
-    };
-
-    try {
-      const mailSettingParams = await updateMailSettinConfig(requestMailSettingParams);
-      return res.apiv3({ mailSettingParams });
-    }
-    catch (err) {
-      const msg = 'Error occurred in updating mail setting';
-      logger.error('Error', err);
-      return res.apiv3Err(new ErrorV3(msg, 'update-mailSetting-failed'));
+      return res.apiv3Err(new ErrorV3(msg, 'send-email-with-smtp-failed'));
     }
   });
 
   /**
    * @swagger
    *
-   *    /app-settings/mail-setting:
-   *      delete:
+   *    /app-settings/ses-setting:
+   *      put:
    *        tags: [AppSettings]
-   *        operationId: deleteAppSettingMailSetting
-   *        summary: /app-settings/mail-setting
-   *        description: delete mail setting
+   *        operationId: updateAppSettingSesSetting
+   *        summary: /app-settings/ses-setting
+   *        description: Update ses setting
    *        requestBody:
    *          required: true
    *          content:
    *            application/json:
    *              schema:
-   *                $ref: '#/components/schemas/MailSettingParams'
+   *                $ref: '#/components/schemas/SesSettingParams'
    *        responses:
    *          200:
-   *            description: Succeeded to delete mail setting
+   *            description: Succeeded to update ses setting
    *            content:
    *              application/json:
    *                schema:
-   *                  $ref: '#/components/schemas/MailSettingParams'
+   *                  $ref: '#/components/schemas/SesSettingParams'
    */
-  router.delete('/mail-setting', loginRequiredStrictly, adminRequired, csrf, async(req, res) => {
-    const requestMailSettingParams = {
-      'mail:smtpHost': null,
-      'mail:smtpPort': null,
-      'mail:smtpUser': null,
-      'mail:smtpPassword': null,
+  router.put('/ses-setting', loginRequiredStrictly, adminRequired, csrf, validator.sesSetting, apiV3FormValidator, async(req, res) => {
+    const { mailService } = crowi;
+
+    const requestSesSettingParams = {
+      'mail:from': req.body.fromAddress,
+      'mail:transmissionMethod': req.body.transmissionMethod,
+      'mail:sesAccessKeyId': req.body.sesAccessKeyId,
+      'mail:sesSecretAccessKey': req.body.sesSecretAccessKey,
     };
+
+    let mailSettingParams;
     try {
-      const mailSettingParams = await updateMailSettinConfig(requestMailSettingParams);
-      return res.apiv3({ mailSettingParams });
+      mailSettingParams = await updateMailSettinConfig(requestSesSettingParams);
     }
     catch (err) {
-      const msg = 'Error occurred in initializing mail setting';
+      const msg = 'Error occurred in updating ses setting';
       logger.error('Error', err);
-      return res.apiv3Err(new ErrorV3(msg, 'initialize-mailSetting-failed'));
+      return res.apiv3Err(new ErrorV3(msg, 'update-ses-setting-failed'));
     }
+
+    await mailService.initialize();
+    mailService.publishUpdatedMessage();
+
+    return res.apiv3({ mailSettingParams });
   });
 
   /**
@@ -516,14 +540,11 @@ module.exports = (crowi) => {
     };
 
     try {
-      const { configManager, mailService } = crowi;
+      const { configManager } = crowi;
 
       // update config without publishing S2sMessage
       await configManager.updateConfigsInTheSameNamespace('crowi', requestAwsSettingParams, true);
 
-      await mailService.initialize();
-      mailService.publishUpdatedMessage();
-
       const awsSettingParams = {
         region: crowi.configManager.getConfig('crowi', 'aws:region'),
         customEndpoint: crowi.configManager.getConfig('crowi', 'aws:customEndpoint'),

+ 32 - 8
src/server/service/mail.js

@@ -20,6 +20,11 @@ class MailService extends S2sMessageHandlable {
     this.mailConfig = {};
     this.mailer = {};
 
+    /**
+     * the flag whether mailer is set up successfully
+     */
+    this.isMailerSetup = false;
+
     this.initialize();
   }
 
@@ -65,23 +70,29 @@ class MailService extends S2sMessageHandlable {
   initialize() {
     const { appService, configManager } = this;
 
+    this.isMailerSetup = false;
+
     if (!configManager.getConfig('crowi', 'mail:from')) {
       this.mailer = null;
       return;
     }
 
-    // Priority 1. SMTP
-    if (configManager.getConfig('crowi', 'mail:smtpHost') && configManager.getConfig('crowi', 'mail:smtpPort')) {
+    const transmissionMethod = configManager.getConfig('crowi', 'mail:transmissionMethod');
+
+    if (transmissionMethod === 'smtp') {
       this.mailer = this.createSMTPClient();
     }
-    // Priority 2. SES
-    else if (configManager.getConfig('crowi', 'aws:accessKeyId') && configManager.getConfig('crowi', 'aws:secretAccessKey')) {
+    else if (transmissionMethod === 'ses') {
       this.mailer = this.createSESClient();
     }
     else {
       this.mailer = null;
     }
 
+    if (this.mailer != null) {
+      this.isMailerSetup = true;
+    }
+
     this.mailConfig.from = configManager.getConfig('crowi', 'mail:from');
     this.mailConfig.subject = `${appService.getAppTitle()}からのメール`;
 
@@ -93,9 +104,15 @@ class MailService extends S2sMessageHandlable {
 
     logger.debug('createSMTPClient option', option);
     if (!option) {
+      const host = configManager.getConfig('crowi', 'mail:smtpHost');
+      const port = configManager.getConfig('crowi', 'mail:smtpPort');
+
+      if (host == null || port == null) {
+        return null;
+      }
       option = { // eslint-disable-line no-param-reassign
-        host: configManager.getConfig('crowi', 'mail:smtpHost'),
-        port: configManager.getConfig('crowi', 'mail:smtpPort'),
+        host,
+        port,
       };
 
       if (configManager.getConfig('crowi', 'mail:smtpUser') && configManager.getConfig('crowi', 'mail:smtpPassword')) {
@@ -113,6 +130,7 @@ class MailService extends S2sMessageHandlable {
     const client = nodemailer.createTransport(option);
 
     logger.debug('mailer set up for SMTP', client);
+
     return client;
   }
 
@@ -120,9 +138,14 @@ class MailService extends S2sMessageHandlable {
     const { configManager } = this;
 
     if (!option) {
+      const accessKeyId = configManager.getConfig('crowi', 'mail:sesAccessKeyId');
+      const secretAccessKey = configManager.getConfig('crowi', 'mail:sesSecretAccessKey');
+      if (accessKeyId == null || secretAccessKey == null) {
+        return null;
+      }
       option = { // eslint-disable-line no-param-reassign
-        accessKeyId: configManager.getConfig('crowi', 'aws:accessKeyId'),
-        secretAccessKey: configManager.getConfig('crowi', 'aws:secretAccessKey'),
+        accessKeyId,
+        secretAccessKey,
       };
     }
 
@@ -130,6 +153,7 @@ class MailService extends S2sMessageHandlable {
     const client = nodemailer.createTransport(ses(option));
 
     logger.debug('mailer set up for SES', client);
+
     return client;
   }