EmailAlert.cs 12 KB

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