/*
Technitium DNS Server
Copyright (C) 2024 Shreyas Zare (shreyas@technitium.com)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
using DnsServerCore.ApplicationCommon;
using System;
using System.Net;
using System.Net.Mail;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using TechnitiumLibrary;
using TechnitiumLibrary.Net.Dns;
using TechnitiumLibrary.Net.Dns.ResourceRecords;
using TechnitiumLibrary.Net.Mail;
namespace Failover
{
class EmailAlert : IDisposable
{
#region variables
readonly HealthService _service;
readonly string _name;
bool _enabled;
MailAddress[] _alertTo;
string _smtpServer;
int _smtpPort;
bool _startTls;
bool _smtpOverTls;
string _username;
string _password;
MailAddress _mailFrom;
readonly SmtpClientEx _smtpClient;
#endregion
#region constructor
public EmailAlert(HealthService service, JsonElement jsonEmailAlert)
{
_service = service;
_smtpClient = new SmtpClientEx();
_smtpClient.DnsClient = new DnsClientInternal(_service.DnsServer);
_name = jsonEmailAlert.GetPropertyValue("name", "default");
Reload(jsonEmailAlert);
}
#endregion
#region IDisposable
bool _disposed;
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
if (_smtpClient is not null)
_smtpClient.Dispose();
}
_disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
#region private
private async Task SendMailAsync(MailMessage message)
{
try
{
const int MAX_RETRIES = 3;
const int WAIT_INTERVAL = 30000;
for (int retries = 0; retries < MAX_RETRIES; retries++)
{
try
{
await _smtpClient.SendMailAsync(message);
break;
}
catch
{
if (retries == MAX_RETRIES - 1)
throw;
await Task.Delay(WAIT_INTERVAL);
}
}
}
catch (Exception ex)
{
_service.DnsServer.WriteLog("Failed to send email alert [" + _name + "].\r\n" + ex.ToString());
}
}
#endregion
#region public
public void Reload(JsonElement jsonEmailAlert)
{
_enabled = jsonEmailAlert.GetPropertyValue("enabled", false);
if (jsonEmailAlert.TryReadArray("alertTo", delegate (string emailAddress) { return new MailAddress(emailAddress); }, out MailAddress[] alertTo))
_alertTo = alertTo;
else
_alertTo = null;
_smtpServer = jsonEmailAlert.GetPropertyValue("smtpServer", null);
_smtpPort = jsonEmailAlert.GetPropertyValue("smtpPort", 25);
_startTls = jsonEmailAlert.GetPropertyValue("startTls", false);
_smtpOverTls = jsonEmailAlert.GetPropertyValue("smtpOverTls", false);
_username = jsonEmailAlert.GetPropertyValue("username", null);
_password = jsonEmailAlert.GetPropertyValue("password", null);
if (jsonEmailAlert.TryGetProperty("mailFrom", out JsonElement jsonMailFrom))
{
if (jsonEmailAlert.TryGetProperty("mailFromName", out JsonElement jsonMailFromName))
_mailFrom = new MailAddress(jsonMailFrom.GetString(), jsonMailFromName.GetString(), Encoding.UTF8);
else
_mailFrom = new MailAddress(jsonMailFrom.GetString());
}
else
{
_mailFrom = null;
}
//update smtp client settings
_smtpClient.Host = _smtpServer;
_smtpClient.Port = _smtpPort;
_smtpClient.EnableSsl = _startTls;
_smtpClient.SmtpOverTls = _smtpOverTls;
if (string.IsNullOrEmpty(_username))
_smtpClient.Credentials = null;
else
_smtpClient.Credentials = new NetworkCredential(_username, _password);
_smtpClient.Proxy = _service.DnsServer.Proxy;
}
public Task SendAlertAsync(IPAddress address, string healthCheck, HealthCheckResponse healthCheckResponse)
{
if (!_enabled || (_mailFrom is null) || (_alertTo is null) || (_alertTo.Length == 0))
return Task.CompletedTask;
MailMessage message = new MailMessage();
message.From = _mailFrom;
foreach (MailAddress alertTo in _alertTo)
message.To.Add(alertTo);
message.Subject = "[Alert] Address [" + address.ToString() + "] Status Is " + healthCheckResponse.Status.ToString().ToUpper();
switch (healthCheckResponse.Status)
{
case HealthStatus.Failed:
message.Body = @"Hi,
The DNS Failover App was successfully able to perform a health check [" + healthCheck + "] on the address [" + address.ToString() + @"] and found that the address failed to respond.
Address: " + address.ToString() + @"
Health Check: " + healthCheck + @"
Status: " + healthCheckResponse.Status.ToString().ToUpper() + @"
Alert Time: " + healthCheckResponse.DateTime.ToString("R") + @"
Failure Reason: " + healthCheckResponse.FailureReason + @"
Regards,
DNS Failover App
";
break;
default:
message.Body = @"Hi,
The DNS Failover App was successfully able to perform a health check [" + healthCheck + "] on the address [" + address.ToString() + @"] and found that the address status was " + healthCheckResponse.Status.ToString().ToUpper() + @".
Address: " + address.ToString() + @"
Health Check: " + healthCheck + @"
Status: " + healthCheckResponse.Status.ToString().ToUpper() + @"
Alert Time: " + healthCheckResponse.DateTime.ToString("R") + @"
Regards,
DNS Failover App
";
break;
}
return SendMailAsync(message);
}
public Task SendAlertAsync(IPAddress address, string healthCheck, Exception ex)
{
if (!_enabled || (_mailFrom is null) || (_alertTo is null) || (_alertTo.Length == 0))
return Task.CompletedTask;
MailMessage message = new MailMessage();
message.From = _mailFrom;
foreach (MailAddress alertTo in _alertTo)
message.To.Add(alertTo);
message.Subject = "[Alert] Address [" + address.ToString() + "] Status Is ERROR";
message.Body = @"Hi,
The DNS Failover App has failed to perform a health check [" + healthCheck + "] on the address [" + address.ToString() + @"].
Address: " + address.ToString() + @"
Health Check: " + healthCheck + @"
Status: ERROR
Alert Time: " + DateTime.UtcNow.ToString("R") + @"
Failure Reason: " + ex.ToString() + @"
Regards,
DNS Failover App
";
return SendMailAsync(message);
}
public Task SendAlertAsync(string domain, DnsResourceRecordType type, string healthCheck, HealthCheckResponse healthCheckResponse)
{
if (!_enabled || (_mailFrom is null) || (_alertTo is null) || (_alertTo.Length == 0))
return Task.CompletedTask;
MailMessage message = new MailMessage();
message.From = _mailFrom;
foreach (MailAddress alertTo in _alertTo)
message.To.Add(alertTo);
message.Subject = "[Alert] Domain [" + domain + "] Status Is " + healthCheckResponse.Status.ToString().ToUpper();
switch (healthCheckResponse.Status)
{
case HealthStatus.Failed:
message.Body = @"Hi,
The DNS Failover App was successfully able to perform a health check [" + healthCheck + "] on the domain name [" + domain + @"] and found that the domain name failed to respond.
Domain: " + domain + @"
Record Type: " + type.ToString() + @"
Health Check: " + healthCheck + @"
Status: " + healthCheckResponse.Status.ToString().ToUpper() + @"
Alert Time: " + healthCheckResponse.DateTime.ToString("R") + @"
Failure Reason: " + healthCheckResponse.FailureReason + @"
Regards,
DNS Failover App
";
break;
default:
message.Body = @"Hi,
The DNS Failover App was successfully able to perform a health check [" + healthCheck + "] on the domain name [" + domain + @"] and found that the domain name status was " + healthCheckResponse.Status.ToString().ToUpper() + @".
Domain: " + domain + @"
Record Type: " + type.ToString() + @"
Health Check: " + healthCheck + @"
Status: " + healthCheckResponse.Status.ToString().ToUpper() + @"
Alert Time: " + healthCheckResponse.DateTime.ToString("R") + @"
Regards,
DNS Failover App
";
break;
}
return SendMailAsync(message);
}
public Task SendAlertAsync(string domain, DnsResourceRecordType type, string healthCheck, Exception ex)
{
if (!_enabled || (_mailFrom is null) || (_alertTo is null) || (_alertTo.Length == 0))
return Task.CompletedTask;
MailMessage message = new MailMessage();
message.From = _mailFrom;
foreach (MailAddress alertTo in _alertTo)
message.To.Add(alertTo);
message.Subject = "[Alert] Domain [" + domain + "] Status Is ERROR";
message.Body = @"Hi,
The DNS Failover App has failed to perform a health check [" + healthCheck + "] on the domain name [" + domain + @"].
Domain: " + domain + @"
Record Type: " + type.ToString() + @"
Health Check: " + healthCheck + @"
Status: ERROR
Alert Time: " + DateTime.UtcNow.ToString("R") + @"
Failure Reason: " + ex.ToString() + @"
Regards,
DNS Failover App
";
return SendMailAsync(message);
}
#endregion
#region properties
public string Name
{ get { return _name; } }
public bool Enabled
{ get { return _enabled; } }
public MailAddress[] AlertTo
{ get { return _alertTo; } }
public string SmtpServer
{ get { return _smtpServer; } }
public int SmtpPort
{ get { return _smtpPort; } }
public bool StartTls
{ get { return _startTls; } }
public bool SmtpOverTls
{ get { return _smtpOverTls; } }
public string Username
{ get { return _username; } }
public string Password
{ get { return _password; } }
public MailAddress MailFrom
{ get { return _mailFrom; } }
#endregion
class DnsClientInternal : IDnsClient
{
readonly IDnsServer _dnsServer;
public DnsClientInternal(IDnsServer dnsServer)
{
_dnsServer = dnsServer;
}
public Task ResolveAsync(DnsQuestionRecord question, CancellationToken cancellationToken = default)
{
return _dnsServer.DirectQueryAsync(question);
}
}
}
}