EmailAlert.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. /*
  2. Technitium DNS Server
  3. Copyright (C) 2024 Shreyas Zare (shreyas@technitium.com)
  4. This program is free software: you can redistribute it and/or modify
  5. it under the terms of the GNU General Public License as published by
  6. the Free Software Foundation, either version 3 of the License, or
  7. (at your option) any later version.
  8. This program is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. GNU General Public License for more details.
  12. You should have received a copy of the GNU General Public License
  13. along with this program. If not, see <http://www.gnu.org/licenses/>.
  14. */
  15. using DnsServerCore.ApplicationCommon;
  16. using System;
  17. using System.Net;
  18. using System.Net.Mail;
  19. using System.Text;
  20. using System.Text.Json;
  21. using System.Threading;
  22. using System.Threading.Tasks;
  23. using TechnitiumLibrary;
  24. using TechnitiumLibrary.Net.Dns;
  25. using TechnitiumLibrary.Net.Dns.ResourceRecords;
  26. using TechnitiumLibrary.Net.Mail;
  27. namespace Failover
  28. {
  29. class EmailAlert : IDisposable
  30. {
  31. #region variables
  32. readonly HealthService _service;
  33. readonly string _name;
  34. bool _enabled;
  35. MailAddress[] _alertTo;
  36. string _smtpServer;
  37. int _smtpPort;
  38. bool _startTls;
  39. bool _smtpOverTls;
  40. string _username;
  41. string _password;
  42. MailAddress _mailFrom;
  43. readonly SmtpClientEx _smtpClient;
  44. #endregion
  45. #region constructor
  46. public EmailAlert(HealthService service, JsonElement jsonEmailAlert)
  47. {
  48. _service = service;
  49. _smtpClient = new SmtpClientEx();
  50. _smtpClient.DnsClient = new DnsClientInternal(_service.DnsServer);
  51. _name = jsonEmailAlert.GetPropertyValue("name", "default");
  52. Reload(jsonEmailAlert);
  53. }
  54. #endregion
  55. #region IDisposable
  56. bool _disposed;
  57. protected virtual void Dispose(bool disposing)
  58. {
  59. if (_disposed)
  60. return;
  61. if (disposing)
  62. {
  63. if (_smtpClient is not null)
  64. _smtpClient.Dispose();
  65. }
  66. _disposed = true;
  67. }
  68. public void Dispose()
  69. {
  70. Dispose(true);
  71. GC.SuppressFinalize(this);
  72. }
  73. #endregion
  74. #region private
  75. private async Task SendMailAsync(MailMessage message)
  76. {
  77. try
  78. {
  79. const int MAX_RETRIES = 3;
  80. const int WAIT_INTERVAL = 30000;
  81. for (int retries = 0; retries < MAX_RETRIES; retries++)
  82. {
  83. try
  84. {
  85. await _smtpClient.SendMailAsync(message);
  86. break;
  87. }
  88. catch
  89. {
  90. if (retries == MAX_RETRIES - 1)
  91. throw;
  92. await Task.Delay(WAIT_INTERVAL);
  93. }
  94. }
  95. }
  96. catch (Exception ex)
  97. {
  98. _service.DnsServer.WriteLog("Failed to send email alert [" + _name + "].\r\n" + ex.ToString());
  99. }
  100. }
  101. #endregion
  102. #region public
  103. public void Reload(JsonElement jsonEmailAlert)
  104. {
  105. _enabled = jsonEmailAlert.GetPropertyValue("enabled", false);
  106. if (jsonEmailAlert.TryReadArray("alertTo", delegate (string emailAddress) { return new MailAddress(emailAddress); }, out MailAddress[] alertTo))
  107. _alertTo = alertTo;
  108. else
  109. _alertTo = null;
  110. _smtpServer = jsonEmailAlert.GetPropertyValue("smtpServer", null);
  111. _smtpPort = jsonEmailAlert.GetPropertyValue("smtpPort", 25);
  112. _startTls = jsonEmailAlert.GetPropertyValue("startTls", false);
  113. _smtpOverTls = jsonEmailAlert.GetPropertyValue("smtpOverTls", false);
  114. _username = jsonEmailAlert.GetPropertyValue("username", null);
  115. _password = jsonEmailAlert.GetPropertyValue("password", null);
  116. if (jsonEmailAlert.TryGetProperty("mailFrom", out JsonElement jsonMailFrom))
  117. {
  118. if (jsonEmailAlert.TryGetProperty("mailFromName", out JsonElement jsonMailFromName))
  119. _mailFrom = new MailAddress(jsonMailFrom.GetString(), jsonMailFromName.GetString(), Encoding.UTF8);
  120. else
  121. _mailFrom = new MailAddress(jsonMailFrom.GetString());
  122. }
  123. else
  124. {
  125. _mailFrom = null;
  126. }
  127. //update smtp client settings
  128. _smtpClient.Host = _smtpServer;
  129. _smtpClient.Port = _smtpPort;
  130. _smtpClient.EnableSsl = _startTls;
  131. _smtpClient.SmtpOverTls = _smtpOverTls;
  132. if (string.IsNullOrEmpty(_username))
  133. _smtpClient.Credentials = null;
  134. else
  135. _smtpClient.Credentials = new NetworkCredential(_username, _password);
  136. _smtpClient.Proxy = _service.DnsServer.Proxy;
  137. }
  138. public Task SendAlertAsync(IPAddress address, string healthCheck, HealthCheckResponse healthCheckResponse)
  139. {
  140. if (!_enabled || (_mailFrom is null) || (_alertTo is null) || (_alertTo.Length == 0))
  141. return Task.CompletedTask;
  142. MailMessage message = new MailMessage();
  143. message.From = _mailFrom;
  144. foreach (MailAddress alertTo in _alertTo)
  145. message.To.Add(alertTo);
  146. message.Subject = "[Alert] Address [" + address.ToString() + "] Status Is " + healthCheckResponse.Status.ToString().ToUpper();
  147. switch (healthCheckResponse.Status)
  148. {
  149. case HealthStatus.Failed:
  150. message.Body = @"Hi,
  151. 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.
  152. Address: " + address.ToString() + @"
  153. Health Check: " + healthCheck + @"
  154. Status: " + healthCheckResponse.Status.ToString().ToUpper() + @"
  155. Alert Time: " + healthCheckResponse.DateTime.ToString("R") + @"
  156. Failure Reason: " + healthCheckResponse.FailureReason + @"
  157. Regards,
  158. DNS Failover App
  159. ";
  160. break;
  161. default:
  162. message.Body = @"Hi,
  163. 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() + @".
  164. Address: " + address.ToString() + @"
  165. Health Check: " + healthCheck + @"
  166. Status: " + healthCheckResponse.Status.ToString().ToUpper() + @"
  167. Alert Time: " + healthCheckResponse.DateTime.ToString("R") + @"
  168. Regards,
  169. DNS Failover App
  170. ";
  171. break;
  172. }
  173. return SendMailAsync(message);
  174. }
  175. public Task SendAlertAsync(IPAddress address, string healthCheck, Exception ex)
  176. {
  177. if (!_enabled || (_mailFrom is null) || (_alertTo is null) || (_alertTo.Length == 0))
  178. return Task.CompletedTask;
  179. MailMessage message = new MailMessage();
  180. message.From = _mailFrom;
  181. foreach (MailAddress alertTo in _alertTo)
  182. message.To.Add(alertTo);
  183. message.Subject = "[Alert] Address [" + address.ToString() + "] Status Is ERROR";
  184. message.Body = @"Hi,
  185. The DNS Failover App has failed to perform a health check [" + healthCheck + "] on the address [" + address.ToString() + @"].
  186. Address: " + address.ToString() + @"
  187. Health Check: " + healthCheck + @"
  188. Status: ERROR
  189. Alert Time: " + DateTime.UtcNow.ToString("R") + @"
  190. Failure Reason: " + ex.ToString() + @"
  191. Regards,
  192. DNS Failover App
  193. ";
  194. return SendMailAsync(message);
  195. }
  196. public Task SendAlertAsync(string domain, DnsResourceRecordType type, string healthCheck, HealthCheckResponse healthCheckResponse)
  197. {
  198. if (!_enabled || (_mailFrom is null) || (_alertTo is null) || (_alertTo.Length == 0))
  199. return Task.CompletedTask;
  200. MailMessage message = new MailMessage();
  201. message.From = _mailFrom;
  202. foreach (MailAddress alertTo in _alertTo)
  203. message.To.Add(alertTo);
  204. message.Subject = "[Alert] Domain [" + domain + "] Status Is " + healthCheckResponse.Status.ToString().ToUpper();
  205. switch (healthCheckResponse.Status)
  206. {
  207. case HealthStatus.Failed:
  208. message.Body = @"Hi,
  209. 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.
  210. Domain: " + domain + @"
  211. Record Type: " + type.ToString() + @"
  212. Health Check: " + healthCheck + @"
  213. Status: " + healthCheckResponse.Status.ToString().ToUpper() + @"
  214. Alert Time: " + healthCheckResponse.DateTime.ToString("R") + @"
  215. Failure Reason: " + healthCheckResponse.FailureReason + @"
  216. Regards,
  217. DNS Failover App
  218. ";
  219. break;
  220. default:
  221. message.Body = @"Hi,
  222. 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() + @".
  223. Domain: " + domain + @"
  224. Record Type: " + type.ToString() + @"
  225. Health Check: " + healthCheck + @"
  226. Status: " + healthCheckResponse.Status.ToString().ToUpper() + @"
  227. Alert Time: " + healthCheckResponse.DateTime.ToString("R") + @"
  228. Regards,
  229. DNS Failover App
  230. ";
  231. break;
  232. }
  233. return SendMailAsync(message);
  234. }
  235. public Task SendAlertAsync(string domain, DnsResourceRecordType type, string healthCheck, Exception ex)
  236. {
  237. if (!_enabled || (_mailFrom is null) || (_alertTo is null) || (_alertTo.Length == 0))
  238. return Task.CompletedTask;
  239. MailMessage message = new MailMessage();
  240. message.From = _mailFrom;
  241. foreach (MailAddress alertTo in _alertTo)
  242. message.To.Add(alertTo);
  243. message.Subject = "[Alert] Domain [" + domain + "] Status Is ERROR";
  244. message.Body = @"Hi,
  245. The DNS Failover App has failed to perform a health check [" + healthCheck + "] on the domain name [" + domain + @"].
  246. Domain: " + domain + @"
  247. Record Type: " + type.ToString() + @"
  248. Health Check: " + healthCheck + @"
  249. Status: ERROR
  250. Alert Time: " + DateTime.UtcNow.ToString("R") + @"
  251. Failure Reason: " + ex.ToString() + @"
  252. Regards,
  253. DNS Failover App
  254. ";
  255. return SendMailAsync(message);
  256. }
  257. #endregion
  258. #region properties
  259. public string Name
  260. { get { return _name; } }
  261. public bool Enabled
  262. { get { return _enabled; } }
  263. public MailAddress[] AlertTo
  264. { get { return _alertTo; } }
  265. public string SmtpServer
  266. { get { return _smtpServer; } }
  267. public int SmtpPort
  268. { get { return _smtpPort; } }
  269. public bool StartTls
  270. { get { return _startTls; } }
  271. public bool SmtpOverTls
  272. { get { return _smtpOverTls; } }
  273. public string Username
  274. { get { return _username; } }
  275. public string Password
  276. { get { return _password; } }
  277. public MailAddress MailFrom
  278. { get { return _mailFrom; } }
  279. #endregion
  280. class DnsClientInternal : IDnsClient
  281. {
  282. readonly IDnsServer _dnsServer;
  283. public DnsClientInternal(IDnsServer dnsServer)
  284. {
  285. _dnsServer = dnsServer;
  286. }
  287. public Task<DnsDatagram> ResolveAsync(DnsQuestionRecord question, CancellationToken cancellationToken = default)
  288. {
  289. return _dnsServer.DirectQueryAsync(question);
  290. }
  291. }
  292. }
  293. }