IGNITE-16829 Support configurable mail smtp host, disabling auth and SSL (#190)

diff --git a/conf/branches.json b/conf/branches.json
index a2f7f1b..c61c3f8 100644
--- a/conf/branches.json
+++ b/conf/branches.json
@@ -95,13 +95,22 @@
     }
   ],
   "notifications": {
-    /* Email sending settings, only smtp.gmail.com is now supported. */
+    /* Email sending settings */
     "email": {
-      /* Username, equal to from: field */
+      /* Username, and 'from' field for emails. */
       "username": "ignitetcbot@gmail.com",
+      "auth": true,
       /** Email password encoded using Password Encoder.  */
-      "pwd": ""
-      //todo ^ specify password
+      "pwd": "",
+      //todo ^ specify password or set auth to false
+      "smtp": {
+        /** Host or IP */
+        "host": "127.0.0.1",
+        /** Default is 25 for non-ssl, 465 for ssl enabled */
+        "port": 465,
+        /** SSL enabled, default true */
+        "ssl": true
+      }
     },
     "channels": [
       {
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/conf/LocalFilesBasedConfig.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/conf/LocalFilesBasedConfig.java
index d6ff6ec..313ce2e 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/conf/LocalFilesBasedConfig.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/conf/LocalFilesBasedConfig.java
@@ -102,11 +102,7 @@
 
     /** {@inheritDoc} */
     @Override public NotificationsConfig notifications() {
-        NotificationsConfig notifications = getConfig().notifications();
-        if (notifications != null && !notifications.isEmpty())
-            return notifications;
-
-        return NotificationsConfig.backwardConfig();
+        return getConfig().notifications();
     }
 
     /** {@inheritDoc} */
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/issue/IssueDetector.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/issue/IssueDetector.java
index 19b0c26..c488331 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/issue/IssueDetector.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/issue/IssueDetector.java
@@ -299,7 +299,7 @@
                     String builds = next.buildIdToIssue.keySet().toString();
                     String subj = "[MTCGA]: " + next.countIssues() + " new failures in builds " + builds + " needs to be handled";
 
-                    emailSender.sendEmail(addr, subj, next.toHtml(), next.toPlainText(), notifications);
+                    emailSender.sendEmail(addr, subj, next.toHtml(), next.toPlainText(), notifications.email());
 
                     sndStat.computeIfAbsent(addr, k -> new AtomicInteger()).incrementAndGet();
                 }
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/Version.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/Version.java
index 706a2db..e947aa7 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/Version.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/Version.java
@@ -28,7 +28,7 @@
     public static final String GITHUB_REF = "https://github.com/apache/ignite-teamcity-bot";
 
     /** TC Bot Version. */
-    public static final String VERSION = "20211207";
+    public static final String VERSION = "20220414";
 
     /** Java version, where Web App is running. */
     public String javaVer;
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/monitoring/MonitoringService.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/monitoring/MonitoringService.java
index b1f2712..e196ec3 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/monitoring/MonitoringService.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/monitoring/MonitoringService.java
@@ -27,6 +27,8 @@
 import org.apache.ignite.tcbot.engine.conf.INotificationChannel;
 import org.apache.ignite.tcbot.engine.conf.ITcBotConfig;
 import org.apache.ignite.tcbot.engine.conf.NotificationsConfig;
+import org.apache.ignite.tcbot.notify.IEmailSender;
+import org.apache.ignite.tcbot.notify.ISendEmailConfig;
 import org.apache.ignite.tcbot.notify.ISlackSender;
 
 import javax.annotation.security.PermitAll;
@@ -37,7 +39,6 @@
 import javax.ws.rs.Produces;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.MediaType;
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Comparator;
@@ -120,13 +121,35 @@
                     slackSender.sendMessage(channel.slack(), "Test Slack notification message!", notifications);
             }
         }
-        catch (IOException e) {
+        catch (Exception e) {
             return new SimpleResult("Failed to send test Slack message: " + e.getMessage());
         }
 
         return new SimpleResult("Ok");
     }
 
+    @POST
+    @PermitAll
+    @Path("testEmailNotification")
+    public SimpleResult testEmailNotification(String address) {
+        IEmailSender emailSender = CtxListener.getInjector(ctx).getInstance(IEmailSender.class);
+
+        ITcBotConfig tcBotConfig = CtxListener.getInjector(ctx).getInstance(ITcBotConfig.class);
+
+        try {
+            NotificationsConfig notifications = tcBotConfig.notifications();
+            String subj = "[MTCGA]: test email notification";
+
+            ISendEmailConfig email = notifications.email();
+            String plainText = "Test Email notification message!";
+            emailSender.sendEmail(address, subj, plainText, plainText, email);
+        } catch (Exception e) {
+            return new SimpleResult("Failed to send test Email message: " + e.getMessage());
+        }
+
+        return new SimpleResult("Ok");
+    }
+
 
     @GET
     @PermitAll
diff --git a/ignite-tc-helper-web/src/main/webapp/monitoring.html b/ignite-tc-helper-web/src/main/webapp/monitoring.html
index 7597a52..aceec3e 100644
--- a/ignite-tc-helper-web/src/main/webapp/monitoring.html
+++ b/ignite-tc-helper-web/src/main/webapp/monitoring.html
@@ -157,6 +157,24 @@
                 alert(JSON.stringify(result));
             }
         });
+
+        return false;
+    }
+
+    function testEmailNotification() {
+        $.ajax({
+            url: "rest/monitoring/testEmailNotification",
+            method: "post",
+            data: $("#emailForm").serialize(),
+            success: function(result) {
+                alert(JSON.stringify(result));
+            },
+            error: function(result) {
+                alert(JSON.stringify(result));
+            }
+        });
+
+        return false;
     }
 
 </script>
@@ -178,6 +196,14 @@
 
 <hr>
 <b>Test Slack notification:</b> <button onclick="testSlackNotification()">Send</button>
+<b>Test Email notification:</b>
+
+<form id="emailForm"><b>Email Address:</b>
+    <input type="text" placeholder="Enter Address" name="address" required>
+</form>
+
+<button onclick="testEmailNotification()">Send</button>
+
 <br>
 
 <div id="loadStatus"></div>
diff --git a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/conf/EmailSettings.java b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/conf/EmailSettings.java
index 0c130f9..47f06f5 100644
--- a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/conf/EmailSettings.java
+++ b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/conf/EmailSettings.java
@@ -16,13 +16,29 @@
  */
 package org.apache.ignite.tcbot.engine.conf;
 
-public class EmailSettings {
+import com.google.common.base.Preconditions;
+import org.apache.ignite.tcbot.common.conf.PasswordEncoder;
+import org.apache.ignite.tcbot.notify.ISendEmailConfig;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+
+@SuppressWarnings("unused")
+public class EmailSettings implements ISendEmailConfig {
     /** Email to send notifications from. */
     private String username;
 
     /** Email password, encoded using Password Encoder. */
     private String pwd;
 
+    /** Custom smtp server, default is gmail */
+    private SmtpSettings smtp;
+
+    /** Default is true. */
+    private Boolean auth;
+
     /**
      * @return Email to send notifications from.
      */
@@ -50,4 +66,63 @@
     public void password(String pwd) {
         this.pwd = pwd;
     }
+
+    @Nullable
+    public SmtpSettings smtp() {
+        return smtp;
+    }
+
+    @Nullable public Boolean isAuthRequired() {
+        return auth;
+    }
+
+    @Nullable
+    @Override
+    public Boolean isSmtpSsl() {
+        SmtpSettings smtp = smtp();
+
+        return smtp != null ? smtp.ssl() : null;
+    }
+
+    @Nullable
+    @Override
+    public Integer smtpPort() {
+        SmtpSettings smtp = smtp();
+
+        return smtp != null ? smtp.port() : null;
+    }
+
+    /**
+     *
+     */
+    @Nonnull
+    @Override public String usernameMandatory() {
+        String username = username();
+
+        Preconditions.checkState(!isNullOrEmpty(username),
+                "notifications/email/username property should be filled in branches.json");
+
+        return username;
+    }
+
+    /**
+     * @return Email password.
+     */
+    @Nonnull
+    @Override public String passwordClearMandatory() {
+        Preconditions.checkState(!isNullOrEmpty(password()),
+                "notifications/email/pwd property should be filled in branches.json");
+
+        return PasswordEncoder.decode(password());
+    }
+
+    @Nullable
+    @Override
+    public String smtpHost() {
+        SmtpSettings smtp = smtp();
+
+        return smtp != null ? smtp.host() : null;
+    }
+
+
 }
diff --git a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/conf/NotificationChannel.java b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/conf/NotificationChannel.java
index 9f1b57d..3ad0b16 100644
--- a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/conf/NotificationChannel.java
+++ b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/conf/NotificationChannel.java
@@ -26,7 +26,7 @@
  *
  */
 public class NotificationChannel implements INotificationChannel {
-    /** (Destionation) Email. */
+    /** (Destination) Email. */
     private String email;
 
     /** Slack. */
@@ -74,14 +74,6 @@
         return (tagsFilter != null) && !tagsFilter.isEmpty();
     }
 
-    public void slack(String slack) {
-        this.slack = slack;
-    }
-
-    public void subscribe(String trackedBranchName) {
-        this.subscribed.add(trackedBranchName);
-    }
-
     /** {@inheritDoc} */
     @Override public String toString() {
         return MoreObjects.toStringHelper(this)
diff --git a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/conf/NotificationsConfig.java b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/conf/NotificationsConfig.java
index 3d4da02..b63c10e 100644
--- a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/conf/NotificationsConfig.java
+++ b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/conf/NotificationsConfig.java
@@ -19,28 +19,23 @@
 
 import com.google.common.base.Preconditions;
 import com.google.common.base.Strings;
-import org.apache.ignite.tcbot.common.conf.ITcServerConfig;
 import org.apache.ignite.tcbot.common.conf.PasswordEncoder;
-import org.apache.ignite.tcbot.common.conf.TcBotWorkDir;
+import org.apache.ignite.tcbot.notify.ISendEmailConfig;
+import org.apache.ignite.tcbot.notify.ISlackBotConfig;
 
-import java.io.File;
-import java.io.FileReader;
-import java.io.IOException;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
-import java.util.Properties;
-import javax.annotation.Nonnull;
-import org.apache.ignite.tcbot.notify.ISendEmailConfig;
-import org.apache.ignite.tcbot.notify.ISlackBotConfig;
 
 import static com.google.common.base.Strings.isNullOrEmpty;
 
 /**
  * Notifications Config
  */
-public class NotificationsConfig implements ISendEmailConfig, ISlackBotConfig {
+public class NotificationsConfig implements ISlackBotConfig {
     /** (Source) Email. */
     private EmailSettings email = new EmailSettings();
 
@@ -48,60 +43,9 @@
     private String slackAuthTok;
 
     /** Channels to send notifications to. */
+    @SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
     private List<NotificationChannel> channels = new ArrayList<>();
 
-    private static final String MAIL_PROPS = "mail.auth.properties";
-    private static final String USERNAME = "username";
-    private static final String ENCODED_PASSWORD = "encoded_password";
-    /** Slack authorization token property name. */
-    private static final String SLACK_AUTH_TOKEN = "slack.auth_token";
-    @Deprecated
-    private static final String SLACK_CHANNEL = "slack.channel";
-
-    @Nonnull
-    public static NotificationsConfig backwardConfig() {
-        Properties cfgProps = loadEmailSettings();
-
-        NotificationsConfig cfg = new NotificationsConfig();
-
-        cfg.slackAuthTok = cfgProps.getProperty(SLACK_AUTH_TOKEN);
-
-        cfg.email.username(cfgProps.getProperty(USERNAME));
-
-        cfg.email.password(cfgProps.getProperty(ENCODED_PASSWORD));
-
-        String slackCh = cfgProps.getProperty(SLACK_CHANNEL);
-        if (!Strings.isNullOrEmpty(slackCh)) {
-            NotificationChannel ch = new NotificationChannel();
-            ch.slack("#" + slackCh);
-            ch.subscribe(ITcServerConfig.DEFAULT_TRACKED_BRANCH_NAME);
-            cfg.channels.add(ch);
-        }
-
-        return cfg;
-    }
-
-    public static Properties loadEmailSettings() {
-        try {
-            return loadProps(new File(TcBotWorkDir.resolveWorkDir(), MAIL_PROPS));
-        }
-        catch (IOException e) {
-            e.printStackTrace();
-            return new Properties();
-        }
-    }
-
-    private static Properties loadProps(File file) throws IOException {
-        Properties props = new Properties();
-
-        try (FileReader reader = new FileReader(file)) {
-            props.load(reader);
-        }
-
-        return props;
-    }
-
-
     public boolean isEmpty() {
         return (email == null || Strings.isNullOrEmpty(email.username()))
             && (email == null || Strings.isNullOrEmpty(email.password()))
@@ -112,50 +56,12 @@
         return slackAuthTok;
     }
 
-    /**
-     * @return Email to send notifications from.
-     */
-    public String emailUsername() {
-        return email == null ? null : email.username();
-    }
-
-    /**
-     *
-     */
-    @Nonnull
-    @Override public String emailUsernameMandatory() {
-        String username = emailUsername();
-
-        Preconditions.checkState(!isNullOrEmpty(username),
-            "notifications/email/username property should be filled in branches.json");
-
-        return username;
-    }
-
-    /**
-     * @return Email password.
-     */
-    @Nonnull
-    @Override public String emailPasswordClearMandatory() {
-        Preconditions.checkNotNull(email,
-            "notifications/email/pwd property should be filled in branches.json");
-        Preconditions.checkState(!isNullOrEmpty(email.password()),
-            "notifications/email/pwd property should be filled in branches.json");
-
-        return PasswordEncoder.decode(email.password());
-    }
 
     public Collection<? extends INotificationChannel> channels() {
-        if (channels == null)
-            return Collections.emptyList();
-
-        return Collections.unmodifiableList(channels);
+        return channels == null ? Collections.emptyList() : Collections.unmodifiableList(channels);
     }
 
-    public void addChannel(NotificationChannel ch) {
-        if (channels == null)
-            this.channels = new ArrayList<>();
-
-        channels.add(ch);
+    public ISendEmailConfig email() {
+        return email;
     }
 }
diff --git a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/conf/SmtpSettings.java b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/conf/SmtpSettings.java
new file mode 100644
index 0000000..4911a05
--- /dev/null
+++ b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/conf/SmtpSettings.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.tcbot.engine.conf;
+
+import javax.annotation.Nullable;
+
+public class SmtpSettings {
+    /** Default is gmail server: "smtp.gmail.com". */
+    private String host;
+
+    /** Default is 465. */
+    private Integer port;
+
+    /** Default is true. */
+    private Boolean ssl;
+
+    @Nullable public String host() {
+        return host;
+    }
+
+    public Integer port() {
+        return port;
+    }
+
+    public Boolean ssl() {
+        return ssl;
+    }
+}
diff --git a/tcbot-notify/src/main/java/org/apache/ignite/tcbot/notify/EmailSender.java b/tcbot-notify/src/main/java/org/apache/ignite/tcbot/notify/EmailSender.java
index 87933d6..6c895bc 100644
--- a/tcbot-notify/src/main/java/org/apache/ignite/tcbot/notify/EmailSender.java
+++ b/tcbot-notify/src/main/java/org/apache/ignite/tcbot/notify/EmailSender.java
@@ -30,39 +30,60 @@
 import javax.mail.internet.MimeMessage;
 import javax.mail.internet.MimeMultipart;
 
+import static com.google.common.base.Strings.isNullOrEmpty;
+
 /**
  * Class for sending email with configured credentials.
  */
 class EmailSender implements IEmailSender {
     /** {@inheritDoc} */
     @Override public void sendEmail(String to, String subject, String html, String plainText,
-        ISendEmailConfig notifications) throws MessagingException {
+        ISendEmailConfig emailConfig) throws MessagingException {
 
-        String user = notifications.emailUsernameMandatory();
+        String user = emailConfig.usernameMandatory();
 
-        String from = user;
+        Authenticator authenticator;
+        Boolean authRequired = emailConfig.isAuthRequired();
+        boolean auth = authRequired == null || authRequired;
+        if (auth) {
+            String pwd = emailConfig.passwordClearMandatory();
 
-        final String pwd = notifications.emailPasswordClearMandatory();
-
-        Properties props = new Properties();
-        props.put("mail.smtp.host", "smtp.gmail.com");
-        props.put("mail.smtp.socketFactory.port", "465");
-        props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
-        props.put("mail.smtp.auth", "true");
-        props.put("mail.smtp.port", "465");
-
-        Session ses = Session.getInstance(props,
-            new Authenticator() {
-                @Override protected PasswordAuthentication getPasswordAuthentication() {
+            authenticator = new Authenticator() {
+                @Override
+                protected PasswordAuthentication getPasswordAuthentication() {
                     return new PasswordAuthentication(user, pwd);
                 }
-            });
+            };
+        } else {
+            authenticator = null;
+        }
+
+        String smtpHost = emailConfig.smtpHost();
+
+        Boolean sslCfg = emailConfig.isSmtpSsl();
+        boolean useSsl = sslCfg == null || sslCfg;
+        int defaultPort = useSsl ? 465 : 25;
+        Integer smtpPortCfg = emailConfig.smtpPort();
+        String smtpPort = Integer.toString(smtpPortCfg == null ? defaultPort : smtpPortCfg);
+
+        Properties props = new Properties();
+        props.put("mail.smtp.host", isNullOrEmpty(smtpHost) ? "smtp.gmail.com" : smtpHost);
+
+        if (useSsl) {
+            props.put("mail.smtp.socketFactory.port", smtpPort);
+            props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
+        }
+
+        props.put("mail.smtp.auth", Boolean.toString(auth));
+        props.put("mail.smtp.port", smtpPort);
+
+        Session ses = Session.getInstance(props, authenticator);
 
         // Create a default MimeMessage object.
         MimeMessage msg = new MimeMessage(ses);
 
         // Set From: header field of the header.
-        msg.setFrom(new InternetAddress(from));
+        msg.setFrom(new InternetAddress(user));
 
         // Set To: header field of the header.
         msg.addRecipient(Message.RecipientType.TO, new InternetAddress(to));
diff --git a/tcbot-notify/src/main/java/org/apache/ignite/tcbot/notify/ISendEmailConfig.java b/tcbot-notify/src/main/java/org/apache/ignite/tcbot/notify/ISendEmailConfig.java
index 307d042..bb51cdf 100644
--- a/tcbot-notify/src/main/java/org/apache/ignite/tcbot/notify/ISendEmailConfig.java
+++ b/tcbot-notify/src/main/java/org/apache/ignite/tcbot/notify/ISendEmailConfig.java
@@ -18,6 +18,7 @@
 package org.apache.ignite.tcbot.notify;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 /**
  * Source email configuration. Now only gmail server is supported.
@@ -27,11 +28,18 @@
      * @return Email username.
      */
     @Nonnull
-    public String emailUsernameMandatory() ;
+    String usernameMandatory() ;
 
     /**
      * @return Email password.
      */
     @Nonnull
-    public String emailPasswordClearMandatory() ;
+    String passwordClearMandatory() ;
+
+    @Nullable Boolean isAuthRequired();
+
+    @Nullable Boolean isSmtpSsl();
+    @Nullable String smtpHost();
+
+    @Nullable Integer smtpPort();
 }