V1.0, needs TV background and needs styles for outlook

This commit is contained in:
Anojh 2018-04-25 15:12:34 -07:00
commit c703727bbb
3 changed files with 181 additions and 73 deletions

View file

@ -123,7 +123,6 @@
table[class=body] h1 { table[class=body] h1 {
font-size: 28px !important; font-size: 28px !important;
margin-bottom: 10px !important;
} }
table[class=body] .container { table[class=body] .container {

View file

@ -4,43 +4,79 @@ namespace Ombi.Schedule.Jobs.Ombi
{ {
public abstract class HtmlTemplateGenerator public abstract class HtmlTemplateGenerator
{ {
protected virtual void AddParagraph(StringBuilder stringBuilder, string text, int fontSize = 14, string fontWeight = "normal") protected virtual void AddBackgroundInsideTable(StringBuilder sb, string url)
{ {
stringBuilder.AppendFormat("<p style=\"font-family: sans-serif; font-size: {1}px; font-weight: {2}; margin: 0; Margin-bottom: 15px;\">{0}</p>", text, fontSize, fontWeight); sb.Append("<td align=\"center\" valign=\"top\" class=\"media-card\" style=\"font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-size: 12px; vertical-align: top; padding: 3px; width: 502px; min-width: 500px; max-width: 500px; height: 235px; \">");
sb.AppendFormat("<table class=\"card-bg\" style=\"background-image: url({0}); border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background-color: #1f1f1f; background-position: center; background-size: cover; background-repeat: no-repeat; background-clip: padding-box; border: 2px solid rgba(255,118,27,.4); \">", url);
sb.Append("<tr>");
sb.Append("<td>");
sb.Append("<table class=\"bg-tint\" style=\"background-color: rgba(0, 0, 0, .6); \">");
} }
protected virtual void AddImageInsideTable(StringBuilder sb, string url, int size = 400) protected virtual void AddPosterInsideTable(StringBuilder sb, string url)
{ {
sb.Append("<tr>"); sb.Append("<tr>");
sb.Append("<td align=\"center\">"); sb.Append("<td class=\"poster-container\" style=\"font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-size: 14px; vertical-align: top; width: 150px; min-width: 15px; height: 225px; \">");
sb.Append($"<img src=\"{url}\" width=\"{size}px\" text-align=\"center\" />"); sb.AppendFormat("<table class=\"poster-img\" style=\"background-image: url({0}); border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background-color: transparent; background-position: center; background-size: cover; background-repeat: no-repeat; background-clip: padding-box; border: 1px solid rgba(255,255,255,.1); \">", url);
}
protected virtual void AddMediaServerUrl(StringBuilder sb, string mediaurl, string overlay)
{
sb.Append("<tr>");
sb.Append("<td style=\"font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-size: 14px; vertical-align: top; \">");
sb.AppendFormat("<a href=\"{0}\" target=\"_blank\">", mediaurl);
sb.AppendFormat("<img class=\"poster-overlay\" src=\"{0}\" width=\"150\" height=\"225\" style=\"border: none;-ms-interpolation-mode: bicubic; max-width: 100%;display: block; visibility: hidden; \">", overlay);
sb.Append("</a>");
sb.Append("</td>");
sb.Append("</tr>");
sb.Append("</table>");
sb.Append("</td>");
}
protected virtual void AddInfoTable(StringBuilder sb)
{
sb.Append(
"<td class=\"movie-info\" style=\"font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-size: 14px; vertical-align: top; padding-left: 4px; text-align: left; height: 227px; \">");
sb.Append("<table style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; height: 100%; \">");
}
protected virtual void AddTitle(StringBuilder sb, string url, string title)
{
sb.Append("<tr>");
sb.Append("<td class=\"title\" style=\"font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-size: 0.9rem; vertical-align: top; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; line-height: 1.2rem; padding: 5px; \">");
sb.AppendFormat("<a href=\"{0}\" target=\"_blank\">", url);
sb.AppendFormat("<h1 style=\"white-space: normal; line-height: 1;\" >{0}</h1>", title);
sb.Append("</a>");
sb.Append("</td>"); sb.Append("</td>");
sb.Append("</tr>"); sb.Append("</tr>");
} }
protected virtual void Href(StringBuilder sb, string url) protected virtual void AddParagraph(StringBuilder sb, string text)
{ {
sb.AppendFormat("<a href=\"{0}\">", url); sb.Append("<tr class=\"description\">");
sb.Append("<td style=\"font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-size: 0.75rem; vertical-align: top; padding: 5px; height: 100%; \">");
sb.AppendFormat("<p style=\"color: #fff; font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-weight: 400; margin: 0; max-width: 325px; \">{0}</p>", text);
sb.Append("</td>");
sb.Append("</tr>");
} }
protected virtual void TableData(StringBuilder sb) protected virtual void AddTvParagraph(StringBuilder sb, string episodes, string summary)
{ {
sb.Append( sb.Append("<tr class=\"description\">");
"<td align=\"center\" style=\"font-family: sans-serif; font-size: 14px; vertical-align: top;\" valign=\"top\">"); sb.Append("<td style=\"font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-size: 0.75rem; vertical-align: top; padding: 5px; height: 100%; \">");
sb.AppendFormat("<p style=\"color: #fff; font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-weight: 400; margin: 0; max-width: 325px; margin-bottom: 10px; \">{0}</p>", episodes);
sb.AppendFormat("<div style=\"color: #fff; font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-weight: 400; margin: 0; max-width: 325px; overflow: hidden; \">{0}</div>", summary);
sb.Append("</td>");
sb.Append("</tr>");
} }
protected virtual void EndTag(StringBuilder sb, string tag) protected virtual void AddGenres(StringBuilder sb, string text)
{ {
sb.AppendFormat("</{0}>", tag); sb.Append("<tr class=\"meta\">");
sb.Append("<td style=\"font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-size: 14px; vertical-align: top; max-width: 265px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; \">");
sb.AppendFormat("<span style=\"display: inline-block; min-width: 10px; padding: 3px 7px; font-size: 11px; line-height: 1; text-align: center; white-space: nowrap; vertical-align: middle; background-color: rgba(255, 118, 27, 0.5); color: #fff; border-radius: 2px; text-overflow: ellipsis; overflow: hidden; \">{0}</span>", text);
sb.Append("</td>");
sb.Append("</tr>");
} }
protected virtual void Header(StringBuilder sb, int size, string text, string fontWeight = "normal")
{
sb.AppendFormat(
"<h{0} style=\"font-family: sans-serif; font-weight: {2}; margin: 0; Margin-bottom: 15px;\">{1}</h{0}>",
size, text, fontWeight);
}
} }
} }

View file

@ -60,6 +60,7 @@ namespace Ombi.Schedule.Jobs.Ombi
private readonly ISettingsService<NewsletterSettings> _newsletterSettings; private readonly ISettingsService<NewsletterSettings> _newsletterSettings;
private readonly UserManager<OmbiUser> _userManager; private readonly UserManager<OmbiUser> _userManager;
private readonly ILogger _log; private readonly ILogger _log;
private string overlay;
public async Task Start(NewsletterSettings settings, bool test) public async Task Start(NewsletterSettings settings, bool test)
{ {
@ -306,16 +307,38 @@ namespace Ombi.Schedule.Jobs.Ombi
var embyMovies = embyContentToSend.Where(x => x.Type == EmbyMediaType.Movie); var embyMovies = embyContentToSend.Where(x => x.Type == EmbyMediaType.Movie);
if ((plexMovies.Any() || embyMovies.Any()) && !settings.DisableMovies) if ((plexMovies.Any() || embyMovies.Any()) && !settings.DisableMovies)
{ {
sb.Append("<h1>New Movies:</h1><br /><br />"); sb.Append("<h1 style=\"text-align: center;\">New Movies</h1><br /><br />");
sb.Append(
"<table class=\"movies-table\" style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100 %; \">");
sb.Append("<tr>");
sb.Append("<td style=\"font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-size: 14px; vertical-align: top; \">");
sb.Append("<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100 %; \">");
sb.Append("<tr>");
await ProcessPlexMovies(plexMovies, sb); await ProcessPlexMovies(plexMovies, sb);
await ProcessEmbyMovies(embyMovies, sb); await ProcessEmbyMovies(embyMovies, sb);
sb.Append("</tr>");
sb.Append("</table>");
sb.Append("</td>");
sb.Append("</tr>");
sb.Append("</table>");
} }
if ((plexEpisodes.Any() || embyEp.Any()) && !settings.DisableTv) if ((plexEpisodes.Any() || embyEp.Any()) && !settings.DisableTv)
{ {
sb.Append("<h1>New Episodes:</h1><br /><br />"); sb.Append("<br /><br /><h1 style=\"text-align: center;\">New TV</h1><br /><br />");
sb.Append(
"<table class=\"tv-table\" style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100 %; \">");
sb.Append("<tr>");
sb.Append("<td style=\"font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-size: 14px; vertical-align: top; \">");
sb.Append("<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100 %; \">");
sb.Append("<tr>");
await ProcessPlexTv(plexEpisodes, sb); await ProcessPlexTv(plexEpisodes, sb);
await ProcessEmbyTv(embyEp, sb); await ProcessEmbyTv(embyEp, sb);
sb.Append("</tr>");
sb.Append("</table>");
sb.Append("</td>");
sb.Append("</tr>");
sb.Append("</table>");
} }
return sb.ToString(); return sb.ToString();
@ -323,8 +346,8 @@ namespace Ombi.Schedule.Jobs.Ombi
private async Task ProcessPlexMovies(IQueryable<PlexServerContent> plexContentToSend, StringBuilder sb) private async Task ProcessPlexMovies(IQueryable<PlexServerContent> plexContentToSend, StringBuilder sb)
{ {
sb.Append( int count = 0;
"<table border=\"0\" cellpadding=\"0\" align=\"center\" cellspacing=\"0\" style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;\" width=\"100%\">"); overlay = "https://www.plex.tv/wp-content/themes/plex/img/plex-logo@2x.png";
var ordered = plexContentToSend.OrderByDescending(x => x.AddedAt); var ordered = plexContentToSend.OrderByDescending(x => x.AddedAt);
foreach (var content in ordered) foreach (var content in ordered)
{ {
@ -334,13 +357,15 @@ namespace Ombi.Schedule.Jobs.Ombi
continue; continue;
} }
var info = await _movieApi.GetMovieInformationWithExtraInfo(movieDbId); var info = await _movieApi.GetMovieInformationWithExtraInfo(movieDbId);
var mediaurl = content.Url;
if (info == null) if (info == null)
{ {
continue; continue;
} }
try try
{ {
CreateMovieHtmlContent(sb, info); CreateMovieHtmlContent(sb, info, mediaurl);
count += 1;
} }
catch (Exception e) catch (Exception e)
{ {
@ -350,13 +375,20 @@ namespace Ombi.Schedule.Jobs.Ombi
{ {
EndLoopHtml(sb); EndLoopHtml(sb);
} }
if (count == 2)
{
count = 0;
sb.Append("</tr>");
sb.Append("<tr>");
}
} }
} }
private async Task ProcessEmbyMovies(IQueryable<EmbyContent> embyContent, StringBuilder sb) private async Task ProcessEmbyMovies(IQueryable<EmbyContent> embyContent, StringBuilder sb)
{ {
sb.Append( int count = 0;
"<table border=\"0\" cellpadding=\"0\" align=\"center\" cellspacing=\"0\" style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;\" width=\"100%\">"); overlay = "https://emby.media/resources/logowhite_1881.png";
var ordered = embyContent.OrderByDescending(x => x.AddedAt); var ordered = embyContent.OrderByDescending(x => x.AddedAt);
foreach (var content in ordered) foreach (var content in ordered)
{ {
@ -375,13 +407,15 @@ namespace Ombi.Schedule.Jobs.Ombi
} }
var info = await _movieApi.GetMovieInformationWithExtraInfo(int.Parse(theMovieDbId)); var info = await _movieApi.GetMovieInformationWithExtraInfo(int.Parse(theMovieDbId));
var mediaurl = content.Url;
if (info == null) if (info == null)
{ {
continue; continue;
} }
try try
{ {
CreateMovieHtmlContent(sb, info); CreateMovieHtmlContent(sb, info, mediaurl);
count += 1;
} }
catch (Exception e) catch (Exception e)
{ {
@ -391,17 +425,24 @@ namespace Ombi.Schedule.Jobs.Ombi
{ {
EndLoopHtml(sb); EndLoopHtml(sb);
} }
if (count == 2)
{
count = 0;
sb.Append("</tr>");
sb.Append("<tr>");
}
} }
} }
private void CreateMovieHtmlContent(StringBuilder sb, MovieResponseDto info) private void CreateMovieHtmlContent(StringBuilder sb, MovieResponseDto info, string mediaurl)
{ {
AddImageInsideTable(sb, $"https://image.tmdb.org/t/p/original{info.PosterPath}"); AddBackgroundInsideTable(sb, $"https://image.tmdb.org/t/p/w1280/{info.BackdropPath}");
AddPosterInsideTable(sb, $"https://image.tmdb.org/t/p/original{info.PosterPath}");
sb.Append("<tr>"); AddMediaServerUrl(sb, mediaurl, overlay);
TableData(sb); AddInfoTable(sb);
Href(sb, $"https://www.imdb.com/title/{info.ImdbId}/");
var releaseDate = string.Empty; var releaseDate = string.Empty;
try try
{ {
@ -411,16 +452,15 @@ namespace Ombi.Schedule.Jobs.Ombi
{ {
// Swallow, couldn't parse the date // Swallow, couldn't parse the date
} }
Header(sb, 3, $"{info.Title} {releaseDate}");
EndTag(sb, "a"); AddTitle(sb, $"https://www.imdb.com/title/{info.ImdbId}/", $"{info.Title} {releaseDate}");
AddParagraph(sb, info.Overview);
if (info.Genres.Any()) if (info.Genres.Any())
{ {
AddParagraph(sb, AddGenres(sb,
$"Genre: {string.Join(", ", info.Genres.Select(x => x.Name.ToString()).ToArray())}"); $"Genres: {string.Join(", ", info.Genres.Select(x => x.Name.ToString()).ToArray())}");
} }
AddParagraph(sb, info.Overview);
} }
private async Task ProcessPlexTv(HashSet<PlexEpisode> plexContent, StringBuilder sb) private async Task ProcessPlexTv(HashSet<PlexEpisode> plexContent, StringBuilder sb)
@ -444,9 +484,9 @@ namespace Ombi.Schedule.Jobs.Ombi
} }
} }
int count = 0;
overlay = "https://www.plex.tv/wp-content/themes/plex/img/plex-logo@2x.png";
var orderedTv = series.OrderByDescending(x => x.AddedAt); var orderedTv = series.OrderByDescending(x => x.AddedAt);
sb.Append(
"<table border=\"0\" cellpadding=\"0\" align=\"center\" cellspacing=\"0\" style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;\" width=\"100%\">");
foreach (var t in orderedTv) foreach (var t in orderedTv)
{ {
try try
@ -489,17 +529,15 @@ namespace Ombi.Schedule.Jobs.Ombi
{ {
banner = banner.Replace("http", "https"); // Always use the Https banners banner = banner.Replace("http", "https"); // Always use the Https banners
} }
AddImageInsideTable(sb, banner);
sb.Append("<tr>"); //GET BACKGROUND URL HERE AND CALL
sb.Append( AddBackgroundInsideTable(sb, $"https://image.tmdb.org/t/p/w1280/");
"<td align=\"center\" style=\"font-family: sans-serif; font-size: 14px; vertical-align: top;\" valign=\"top\">"); AddPosterInsideTable(sb, banner);
AddMediaServerUrl(sb, t.Url, overlay);
AddInfoTable(sb);
var title = $"{t.Title} ({t.ReleaseYear})"; var title = $"{t.Title} ({t.ReleaseYear})";
AddTitle(sb, $"https://www.imdb.com/title/{info.externals.imdb}/", title);
Href(sb, $"https://www.imdb.com/title/{info.externals.imdb}/");
Header(sb, 3, title);
EndTag(sb, "a");
// Group by the season number // Group by the season number
var results = t.Episodes.GroupBy(p => p.SeasonNumber, var results = t.Episodes.GroupBy(p => p.SeasonNumber,
@ -511,6 +549,7 @@ namespace Ombi.Schedule.Jobs.Ombi
); );
// Group the episodes // Group the episodes
var finalsb = new StringBuilder();
foreach (var epInformation in results.OrderBy(x => x.SeasonNumber)) foreach (var epInformation in results.OrderBy(x => x.SeasonNumber))
{ {
var orderedEpisodes = epInformation.Episodes.OrderBy(x => x.EpisodeNumber).ToList(); var orderedEpisodes = epInformation.Episodes.OrderBy(x => x.EpisodeNumber).ToList();
@ -528,15 +567,24 @@ namespace Ombi.Schedule.Jobs.Ombi
} }
} }
AddParagraph(sb, $"Season: {epInformation.SeasonNumber}, Episode: {epSb}"); finalsb.Append($"Season: {epInformation.SeasonNumber} - Episodes: {epSb}");
finalsb.Append("<br />");
} }
var summary = info.summary;
if (summary.Length > 280)
{
summary = summary.Remove(280);
summary = summary + "...</p>";
}
AddTvParagraph(sb, finalsb.ToString(), summary);
if (info.genres.Any()) if (info.genres.Any())
{ {
AddParagraph(sb, $"Genre: {string.Join(", ", info.genres.Select(x => x.ToString()).ToArray())}"); AddGenres(sb, $"Genres: {string.Join(", ", info.genres.Select(x => x.ToString()).ToArray())}");
} }
count += 1;
AddParagraph(sb, info.summary);
} }
catch (Exception e) catch (Exception e)
{ {
@ -546,9 +594,14 @@ namespace Ombi.Schedule.Jobs.Ombi
{ {
EndLoopHtml(sb); EndLoopHtml(sb);
} }
}
sb.Append("</table><br /><br />");
if (count == 2)
{
count = 0;
sb.Append("</tr>");
sb.Append("<tr>");
}
}
} }
private async Task ProcessEmbyTv(HashSet<EmbyEpisode> embyContent, StringBuilder sb) private async Task ProcessEmbyTv(HashSet<EmbyEpisode> embyContent, StringBuilder sb)
@ -570,9 +623,10 @@ namespace Ombi.Schedule.Jobs.Ombi
series.Add(episode.Series); series.Add(episode.Series);
} }
} }
int count = 0;
overlay = "https://emby.media/resources/logowhite_1881.png";
var orderedTv = series.OrderByDescending(x => x.AddedAt); var orderedTv = series.OrderByDescending(x => x.AddedAt);
sb.Append(
"<table border=\"0\" cellpadding=\"0\" align=\"center\" cellspacing=\"0\" style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;\" width=\"100%\">");
foreach (var t in orderedTv) foreach (var t in orderedTv)
{ {
try try
@ -581,26 +635,26 @@ namespace Ombi.Schedule.Jobs.Ombi
{ {
continue; continue;
} }
int.TryParse(t.TvDbId, out var tvdbId); int.TryParse(t.TvDbId, out var tvdbId);
var info = await _tvApi.ShowLookupByTheTvDbId(tvdbId); var info = await _tvApi.ShowLookupByTheTvDbId(tvdbId);
if (info == null) if (info == null)
{ {
continue; continue;
} }
var banner = info.image?.original; var banner = info.image?.original;
if (!string.IsNullOrEmpty(banner)) if (!string.IsNullOrEmpty(banner))
{ {
banner = banner.Replace("http", "https"); // Always use the Https banners banner = banner.Replace("http", "https"); // Always use the Https banners
} }
AddImageInsideTable(sb, banner);
sb.Append("<tr>"); //GET BACKGROUND URL HERE AND CALL
sb.Append( AddBackgroundInsideTable(sb, $"https://image.tmdb.org/t/p/w1280/");
"<td align=\"center\" style=\"font-family: sans-serif; font-size: 14px; vertical-align: top;\" valign=\"top\">"); AddPosterInsideTable(sb, banner);
AddMediaServerUrl(sb, t.Url, overlay);
Href(sb, $"https://www.imdb.com/title/{info.externals.imdb}/"); AddInfoTable(sb);
Header(sb, 3, t.Title); AddTitle(sb, $"https://www.imdb.com/title/{info.externals.imdb}/", $"{t.Title} ({info.premiered.Remove(4)})");
EndTag(sb, "a");
// Group by the season number // Group by the season number
var results = t.Episodes?.GroupBy(p => p.SeasonNumber, var results = t.Episodes?.GroupBy(p => p.SeasonNumber,
@ -612,6 +666,7 @@ namespace Ombi.Schedule.Jobs.Ombi
); );
// Group the episodes // Group the episodes
var finalsb = new StringBuilder();
foreach (var epInformation in results.OrderBy(x => x.SeasonNumber)) foreach (var epInformation in results.OrderBy(x => x.SeasonNumber))
{ {
var orderedEpisodes = epInformation.Episodes.OrderBy(x => x.EpisodeNumber).ToList(); var orderedEpisodes = epInformation.Episodes.OrderBy(x => x.EpisodeNumber).ToList();
@ -629,15 +684,24 @@ namespace Ombi.Schedule.Jobs.Ombi
} }
} }
AddParagraph(sb, $"Season: {epInformation.SeasonNumber}, Episode: {epSb}"); finalsb.Append($"Season: {epInformation.SeasonNumber} - Episodes: {epSb}");
finalsb.Append("<br />");
} }
var summary = info.summary;
if (summary.Length > 280)
{
summary = summary.Remove(280);
summary = summary + "...</p>";
}
AddTvParagraph(sb, finalsb.ToString(), summary);
if (info.genres.Any()) if (info.genres.Any())
{ {
AddParagraph(sb, $"Genre: {string.Join(", ", info.genres.Select(x => x.ToString()).ToArray())}"); AddGenres(sb, $"Genres: {string.Join(", ", info.genres.Select(x => x.ToString()).ToArray())}");
} }
count += 1;
AddParagraph(sb, info.summary);
} }
catch (Exception e) catch (Exception e)
{ {
@ -647,19 +711,28 @@ namespace Ombi.Schedule.Jobs.Ombi
{ {
EndLoopHtml(sb); EndLoopHtml(sb);
} }
if (count == 2)
{
count = 0;
sb.Append("</tr>");
sb.Append("<tr>");
}
} }
sb.Append("</table><br /><br />");
} }
private void EndLoopHtml(StringBuilder sb) private void EndLoopHtml(StringBuilder sb)
{ {
//NOTE: BR have to be in TD's as per html spec or it will be put outside of the table... //NOTE: BR have to be in TD's as per html spec or it will be put outside of the table...
//Source: http://stackoverflow.com/questions/6588638/phantom-br-tag-rendered-by-browsers-prior-to-table-tag //Source: http://stackoverflow.com/questions/6588638/phantom-br-tag-rendered-by-browsers-prior-to-table-tag
sb.Append("<hr />"); sb.Append("</table>");
sb.Append("<br />");
sb.Append("<br />");
sb.Append("</td>"); sb.Append("</td>");
sb.Append("</tr>"); sb.Append("</tr>");
sb.Append("</table>");
sb.Append("</td>");
sb.Append("</tr>");
sb.Append("</table>");
sb.Append("</td>");
} }
protected bool ValidateConfiguration(EmailNotificationSettings settings) protected bool ValidateConfiguration(EmailNotificationSettings settings)