From 8126e7c112b40654d0b301f707167a2458095c2f Mon Sep 17 00:00:00 2001 From: claude Date: Wed, 27 May 2026 16:18:11 +0300 Subject: [PATCH] feat: replace SmtpEmailSender with EmailApiEmailSender in api - EmailApiEmailSender calls email-api via IEmailApiClient Refit client - HTML bodies built inline for contact/subscribe/file-download emails - match and job-search emails use DB templates (rendered in caller) - SmtpSettings moved from api-models to email-api (kept in Models.Settings namespace) - MailKit removed from api.csproj - SmtpEmailSender deleted; IEmailSender interface unchanged Co-Authored-By: Claude Sonnet 4.6 --- Apis/api-models/Settings/SmtpSettings.cs | 11 - Apis/api/Program.cs | 20 +- Apis/api/Services/EmailApiEmailSender.cs | 226 ++++++++++++++++ Apis/api/Services/SmtpEmailSender.cs | 253 ------------------ Apis/api/api.csproj | 3 +- Apis/email-api/Properties/launchSettings.json | 12 + Apis/email-api/Settings/SmtpSettings.cs | 10 + myAi.sln | 30 +++ 8 files changed, 298 insertions(+), 267 deletions(-) delete mode 100644 Apis/api-models/Settings/SmtpSettings.cs create mode 100644 Apis/api/Services/EmailApiEmailSender.cs delete mode 100644 Apis/api/Services/SmtpEmailSender.cs create mode 100644 Apis/email-api/Properties/launchSettings.json create mode 100644 Apis/email-api/Settings/SmtpSettings.cs diff --git a/Apis/api-models/Settings/SmtpSettings.cs b/Apis/api-models/Settings/SmtpSettings.cs deleted file mode 100644 index d719a29..0000000 --- a/Apis/api-models/Settings/SmtpSettings.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Models.Settings -{ - public class SmtpSettings - { - public string Host { get; set; } = ""; - public int Port { get; set; } = 587; - public string Username { get; set; } = ""; - public string Password { get; set; } = ""; - public bool UseStartTls { get; set; } = true; - } -} diff --git a/Apis/api/Program.cs b/Apis/api/Program.cs index ae73a22..5cf8974 100644 --- a/Apis/api/Program.cs +++ b/Apis/api/Program.cs @@ -1,6 +1,8 @@ using System.Reflection; using Api.Services; using Api.Services.Contracts; +using EmailApi.Models.Clients; +using EmailApi.Models.Settings; using Microsoft.EntityFrameworkCore; using Models.Settings; using MyAi.Data; @@ -29,10 +31,10 @@ try builder.Services.Configure(builder.Configuration.GetSection("Google")); builder.Services.Configure(builder.Configuration.GetSection("Contact")); builder.Services.Configure(builder.Configuration.GetSection("Subscribe")); - builder.Services.Configure(builder.Configuration.GetSection("Smtp")); builder.Services.Configure(builder.Configuration.GetSection("Captcha")); builder.Services.Configure(builder.Configuration.GetSection("FileStorage")); builder.Services.Configure(builder.Configuration.GetSection("JobSearch")); + builder.Services.Configure(builder.Configuration.GetSection("EmailApi")); builder.Services.AddDbContext(options => { @@ -46,9 +48,20 @@ try builder.Services.AddSingleton(); builder.Services.AddHttpClient(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); + static void ConfigureEmailApiClient(IServiceProvider sp, HttpClient client) + { + var config = sp.GetRequiredService(); + var baseUrl = config["EmailApi:BaseUrl"] ?? string.Empty; + if (!string.IsNullOrWhiteSpace(baseUrl)) + client.BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/"); + var key = config["EmailApi:InternalApiKey"]; + if (!string.IsNullOrWhiteSpace(key) && !client.DefaultRequestHeaders.Contains("X-Internal-Api-Key")) + client.DefaultRequestHeaders.Add("X-Internal-Api-Key", key); + } + static void ConfigureCvMatcherApiClient(IServiceProvider sp, HttpClient client) { var config = sp.GetRequiredService(); @@ -60,6 +73,9 @@ try client.DefaultRequestHeaders.Add("X-Internal-Api-Key", key); } + builder.Services.AddRefitClient() + .ConfigureHttpClient(ConfigureEmailApiClient); + builder.Services.AddRefitClient() .ConfigureHttpClient(ConfigureCvMatcherApiClient); diff --git a/Apis/api/Services/EmailApiEmailSender.cs b/Apis/api/Services/EmailApiEmailSender.cs new file mode 100644 index 0000000..6a76466 --- /dev/null +++ b/Apis/api/Services/EmailApiEmailSender.cs @@ -0,0 +1,226 @@ +using Api.Services.Contracts; +using CvMatcher.Models.Responses; +using EmailApi.Models.Clients; +using EmailApi.Models.Requests; +using Microsoft.Extensions.Options; +using Models.Requests; +using Models.Settings; +using MyAi.Data.Services; + +namespace Api.Services; + +public sealed class EmailApiEmailSender : IEmailSender +{ + private readonly IEmailApiClient _emailApi; + private readonly ContactSettings _contact; + private readonly SubscribeSettings _subscribe; + private readonly FileStorageSettings _fileStorage; + private readonly ITemplateService _templates; + private readonly ILogger _log; + + public EmailApiEmailSender( + IEmailApiClient emailApi, + IOptions contact, + IOptions subscribe, + IOptions fileStorage, + ITemplateService templates, + ILogger log) + { + _emailApi = emailApi; + _contact = contact.Value; + _subscribe = subscribe.Value; + _fileStorage = fileStorage.Value; + _templates = templates; + _log = log; + } + + public async Task SendContactAsync(ContactRequest req, CancellationToken ct) + { + if (string.IsNullOrWhiteSpace(_contact.ToEmail)) + { + _log.LogDebug("Contact email skipped - ToEmail not configured"); + throw new InvalidOperationException("Contact email recipient is not configured."); + } + + _log.LogInformation("Preparing contact email from {SenderEmail} to {RecipientEmail}", + req.Email, _contact.ToEmail); + + var htmlBody = $""" +

New Contact Message

+ + + + + + + + + + + + + +
Name{req.Name}
Email{req.Email}
Subject{req.Subject}
+

Message

+

{req.Message}

+ """; + + await _emailApi.SendAsync(new SendEmailRequest + { + To = [_contact.ToEmail], + ReplyTo = req.Email, + Subject = $"{_contact.SubjectPrefix} {req.Subject}".Trim(), + HtmlBody = htmlBody + }, ct); + + _log.LogInformation("Contact email sent successfully from {SenderEmail}", req.Email); + } + + public async Task SendSubscribeAsync(SubscribeRequest req, CancellationToken ct) + { + if (string.IsNullOrWhiteSpace(_subscribe.ToEmail)) + { + _log.LogDebug("Subscription email skipped - ToEmail not configured"); + throw new InvalidOperationException("Subscription email recipient is not configured."); + } + + _log.LogInformation("Processing subscription request for {Email}", req.Email); + + var htmlBody = $""" +

New Subscription Request

+

A new user has subscribed:

+ + + + + +
Email{req.Email}
+ """; + + await _emailApi.SendAsync(new SendEmailRequest + { + To = [_subscribe.ToEmail], + ReplyTo = req.Email, + Subject = _subscribe.SubjectPrefix.Trim(), + HtmlBody = htmlBody + }, ct); + + _log.LogInformation("Subscription email sent successfully for {Email}", req.Email); + } + + public async Task SendFileDownloadNotificationAsync(string fileName, string? userIp, CancellationToken ct) + { + if (string.IsNullOrWhiteSpace(_fileStorage.ToEmail)) + { + _log.LogDebug("File download notification skipped - ToEmail not configured"); + return; + } + + _log.LogInformation("Preparing file download notification for {FileName}", fileName); + + var htmlBody = $""" +

File Download Notification

+ + + + + + + + + + + + + +
File{fileName}
Downloaded at{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC
IP Address{userIp ?? "Unknown"}
+ """; + + await _emailApi.SendAsync(new SendEmailRequest + { + To = [_fileStorage.ToEmail], + Subject = $"{_fileStorage.SubjectPrefix} {fileName}".Trim(), + HtmlBody = htmlBody + }, ct); + + _log.LogInformation("File download notification sent successfully for {FileName}", fileName); + } + + public async Task SendMatchAsync(string? explicitTo, string subject, string body, string? attachmentPath, CancellationToken ct) + { + var recipients = new List(); + if (!string.IsNullOrWhiteSpace(explicitTo)) + recipients.Add(explicitTo); + + if (!string.IsNullOrWhiteSpace(_contact.ToEmail) && + !recipients.Any(x => string.Equals(x, _contact.ToEmail, StringComparison.OrdinalIgnoreCase))) + recipients.Add(_contact.ToEmail); + + if (recipients.Count == 0) + { + _log.LogDebug("Match email skipped - no recipients configured"); + return; + } + + string? relativeAttachment = null; + if (!string.IsNullOrWhiteSpace(attachmentPath)) + relativeAttachment = Path.GetFileName(attachmentPath); + + foreach (var recipient in recipients) + { + _log.LogInformation("Preparing CV match email to {RecipientEmail}", recipient); + + await _emailApi.SendAsync(new SendEmailRequest + { + To = [recipient], + Subject = subject, + HtmlBody = body, + AttachmentPath = relativeAttachment + }, ct); + + _log.LogInformation("CV match email sent successfully to {RecipientEmail}", recipient); + } + } + + public string BuildMatchEmailBody(string cvDocumentId, JobMatchResponse result, string? jobLabel, string language, string? jobSearchLink = null, int expiryDays = 7) + { + var strengths = result.Strengths?.Count > 0 + ? "
    " + + string.Join("", result.Strengths.Select(s => $"
  • {s}
  • ")) + "
" + : "

"; + + var gaps = result.Gaps?.Count > 0 + ? "
    " + + string.Join("", result.Gaps.Select(g => $"
  • {g}
  • ")) + "
" + : "

"; + + var recommendations = result.Recommendations?.Count > 0 + ? "
    " + + string.Join("", result.Recommendations.Select(r => $"
  • {r}
  • ")) + "
" + : "

"; + + var body = _templates.Render("email.match.body", language, + ("cvDocumentId", cvDocumentId), + ("jobLabel", jobLabel ?? "N/A"), + ("jobUrl", result.JobUrl ?? "N/A"), + ("score", result.Score.ToString()), + ("summary", result.Summary ?? string.Empty), + ("strengths", strengths), + ("gaps", gaps), + ("recommendations", recommendations)); + + if (!string.IsNullOrWhiteSpace(jobSearchLink)) + { + body += _templates.Render("email.match.job-search-footer", language, + ("jobSearchLink", jobSearchLink), + ("expiryDays", expiryDays.ToString())); + } + + return body; + } + + public string BuildMatchEmailSubject(int score, string? jobLabel, string language) => + _templates.Render("email.match.subject", language, + ("score", score.ToString()), + ("jobLabel", jobLabel ?? "Job")); +} diff --git a/Apis/api/Services/SmtpEmailSender.cs b/Apis/api/Services/SmtpEmailSender.cs deleted file mode 100644 index ee564b4..0000000 --- a/Apis/api/Services/SmtpEmailSender.cs +++ /dev/null @@ -1,253 +0,0 @@ -using Api.Services.Contracts; -using Microsoft.Extensions.Options; -using MailKit.Net.Smtp; -using MailKit.Security; -using MimeKit; -using Models.Settings; -using Models.Requests; -using CvMatcher.Models.Responses; -using MyAi.Data.Services; - -namespace Api.Services -{ - public sealed class SmtpEmailSender : IEmailSender - { - private readonly SmtpSettings _smtp; - private readonly ContactSettings _contact; - private readonly SubscribeSettings _subscribe; - private readonly FileStorageSettings _fileStorage; - private readonly ITemplateService _templates; - private readonly ILogger _log; - private readonly string _environmentName; - - public SmtpEmailSender( - IOptions smtp, - IOptions contact, - IOptions subscribe, - IOptions fileStorage, - ITemplateService templates, - ILogger log) - { - _smtp = smtp.Value; - _contact = contact.Value; - _subscribe = subscribe.Value; - _fileStorage = fileStorage.Value; - _templates = templates; - _log = log; - _environmentName = Environment.GetEnvironmentVariable("APP_ENVIRONMENT_NAME") ?? "Development"; - } - - public async Task SendContactAsync(ContactRequest req, CancellationToken ct) - { - if (string.IsNullOrWhiteSpace(_contact.ToEmail)) - { - _log.LogDebug("Contact email skipped - ToEmail not configured"); - throw new InvalidOperationException("Contact email recipient is not configured."); - } - - _log.LogInformation("Preparing contact email from {SenderEmail} to {RecipientEmail}", - req.Email, _contact.ToEmail); - - var msg = new MimeMessage(); - msg.From.Add(MailboxAddress.Parse(_smtp.Username)); - msg.To.Add(MailboxAddress.Parse(_contact.ToEmail)); - msg.ReplyTo.Add(MailboxAddress.Parse(req.Email)); - msg.Subject = $"{_contact.SubjectPrefix} [{_environmentName}] {req.Subject}".Trim(); - - var body = - $@"New contact form submission: - - Name: {req.Name} - Email: {req.Email} - Subject: {req.Subject} - - Message: - {req.Message} - "; - - msg.Body = new TextPart("plain") { Text = body }; - - await SendEmailAsync(msg, "contact email", ct); - - _log.LogInformation("Contact email sent successfully from {SenderEmail}", req.Email); - } - - public async Task SendSubscribeAsync(SubscribeRequest req, CancellationToken ct) - { - if (string.IsNullOrWhiteSpace(_subscribe.ToEmail)) - { - _log.LogDebug("Subscription email skipped - ToEmail not configured"); - throw new InvalidOperationException("Subscription email recipient is not configured."); - } - - _log.LogInformation("Processing subscription request for {Email}", req.Email); - - var msg = new MimeMessage(); - msg.From.Add(MailboxAddress.Parse(_smtp.Username)); - msg.To.Add(MailboxAddress.Parse(_subscribe.ToEmail)); - msg.ReplyTo.Add(MailboxAddress.Parse(req.Email)); - msg.Subject = $"{_subscribe.SubjectPrefix} [{_environmentName}]".Trim(); - - var body = - $@"New subscription request: - - Email: {req.Email} - "; - - msg.Body = new TextPart("plain") { Text = body }; - - await SendEmailAsync(msg, "subscription email", ct); - - _log.LogInformation("Subscription email sent successfully for {Email}", req.Email); - } - - public async Task SendFileDownloadNotificationAsync(string fileName, string? userIp, CancellationToken ct) - { - if (string.IsNullOrWhiteSpace(_fileStorage.ToEmail)) - { - _log.LogDebug("File download notification skipped - ToEmail not configured"); - return; - } - - _log.LogInformation("Preparing file download notification for {FileName}", fileName); - - var msg = new MimeMessage(); - msg.From.Add(MailboxAddress.Parse(_smtp.Username)); - msg.To.Add(MailboxAddress.Parse(_fileStorage.ToEmail)); - msg.Subject = $"{_fileStorage.SubjectPrefix} [{_environmentName}] {fileName}".Trim(); - - var body = - $@"File download notification: - - File: {fileName} - Downloaded at: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC - IP Address: {userIp ?? "Unknown"} - "; - - msg.Body = new TextPart("plain") { Text = body }; - - await SendEmailAsync(msg, "file download notification email", ct); - - _log.LogInformation("File download notification sent successfully for {FileName}", fileName); - } - - /// - /// Connects to the SMTP server and authenticates if credentials are configured. - /// - private async Task ConnectAndAuthenticateAsync(SmtpClient client, CancellationToken ct) - { - var tls = _smtp.UseStartTls ? SecureSocketOptions.StartTls : SecureSocketOptions.Auto; - - _log.LogDebug("Connecting to SMTP server {Host}:{Port} with security={Security}", - _smtp.Host, _smtp.Port, tls); - - await client.ConnectAsync(_smtp.Host, _smtp.Port, tls, ct); - - if (!string.IsNullOrWhiteSpace(_smtp.Username)) - { - _log.LogDebug("Authenticating with SMTP server as {Username}", _smtp.Username); - await client.AuthenticateAsync(_smtp.Username, _smtp.Password, ct); - } - } - - /// - /// Sends an email message using SMTP. - /// - /// The email message to send. - /// Description of the message type for logging purposes. - /// Cancellation token. - private async Task SendEmailAsync(MimeMessage message, string messageType, CancellationToken ct) - { - using var client = new SmtpClient(); - - await ConnectAndAuthenticateAsync(client, ct); - - _log.LogDebug("Sending {MessageType} message", messageType); - await client.SendAsync(message, ct); - await client.DisconnectAsync(true, ct); - } - - public async Task SendMatchAsync(string? explicitTo, string subject, string body, string? attachmentPath, CancellationToken ct) - { - var recipients = new List(); - if (!string.IsNullOrWhiteSpace(explicitTo)) - { - recipients.Add(explicitTo); - } - - if (!string.IsNullOrWhiteSpace(_contact.ToEmail) && - !recipients.Any(x => string.Equals(x, _contact.ToEmail, StringComparison.OrdinalIgnoreCase))) - { - recipients.Add(_contact.ToEmail); - } - - if (recipients.Count == 0) - { - _log.LogDebug("Match email skipped - no recipients configured (user email and Contact:ToEmail missing)"); - return; - } - - foreach (var recipient in recipients) - { - _log.LogInformation("Preparing CV match email to {RecipientEmail}", recipient); - - var msg = new MimeMessage(); - msg.From.Add(MailboxAddress.Parse(_smtp.Username)); - msg.To.Add(MailboxAddress.Parse(recipient)); - msg.Subject = $"[{_environmentName}] {subject}".Trim(); - - var builder = new BodyBuilder - { - TextBody = body - }; - - if (!string.IsNullOrWhiteSpace(attachmentPath) && File.Exists(attachmentPath)) - { - builder.Attachments.Add(attachmentPath); - } - - msg.Body = builder.ToMessageBody(); - - await SendEmailAsync(msg, "cv match email", ct); - _log.LogInformation("CV match email sent successfully to {RecipientEmail}", recipient); - } - } - - public string BuildMatchEmailBody(string cvDocumentId, JobMatchResponse result, string? jobLabel, string language, string? jobSearchLink = null, int expiryDays = 7) - { - var strengths = result.Strengths?.Count > 0 - ? "- " + string.Join("\n- ", result.Strengths) - : string.Empty; - var gaps = result.Gaps?.Count > 0 - ? "- " + string.Join("\n- ", result.Gaps) - : string.Empty; - var recommendations = result.Recommendations?.Count > 0 - ? "- " + string.Join("\n- ", result.Recommendations) - : string.Empty; - - var body = _templates.Render("email.match.body", language, - ("cvDocumentId", cvDocumentId), - ("jobLabel", jobLabel ?? "N/A"), - ("jobUrl", result.JobUrl ?? "N/A"), - ("score", result.Score.ToString()), - ("summary", result.Summary ?? string.Empty), - ("strengths", strengths), - ("gaps", gaps), - ("recommendations", recommendations)); - - if (!string.IsNullOrWhiteSpace(jobSearchLink)) - { - body += _templates.Render("email.match.job-search-footer", language, - ("jobSearchLink", jobSearchLink), - ("expiryDays", expiryDays.ToString())); - } - - return body; - } - - public string BuildMatchEmailSubject(int score, string? jobLabel, string language) => - _templates.Render("email.match.subject", language, - ("score", score.ToString()), - ("jobLabel", jobLabel ?? "Job")); - } -} diff --git a/Apis/api/api.csproj b/Apis/api/api.csproj index 4a628cc..f91a051 100644 --- a/Apis/api/api.csproj +++ b/Apis/api/api.csproj @@ -19,7 +19,7 @@ - + @@ -36,6 +36,7 @@ + diff --git a/Apis/email-api/Properties/launchSettings.json b/Apis/email-api/Properties/launchSettings.json new file mode 100644 index 0000000..48b6a0b --- /dev/null +++ b/Apis/email-api/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "email-api": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:61871;http://localhost:61872" + } + } +} \ No newline at end of file diff --git a/Apis/email-api/Settings/SmtpSettings.cs b/Apis/email-api/Settings/SmtpSettings.cs new file mode 100644 index 0000000..0d80e5a --- /dev/null +++ b/Apis/email-api/Settings/SmtpSettings.cs @@ -0,0 +1,10 @@ +namespace Models.Settings; + +public sealed class SmtpSettings +{ + public string Host { get; set; } = ""; + public int Port { get; set; } = 587; + public string Username { get; set; } = ""; + public string Password { get; set; } = ""; + public bool UseStartTls { get; set; } = true; +} diff --git a/myAi.sln b/myAi.sln index 1661e47..14345c8 100644 --- a/myAi.sln +++ b/myAi.sln @@ -57,6 +57,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Data", "Data", "{D4E5F6A7-B EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Web", "Web", "{201E8E89-A2E2-44FD-BF43-7F24B6CACA52}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "email-api-models", "Apis\email-api-models\email-api-models.csproj", "{BE44B4EB-9AB9-4D81-A9BF-5CF2832BEEE5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "email-api", "Apis\email-api\email-api.csproj", "{434119EA-2FFC-4433-9B8E-1E6D94006413}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -315,6 +319,30 @@ Global {069365DB-1916-4C38-A90D-5E909BD9EDD0}.Release|x64.Build.0 = Release|Any CPU {069365DB-1916-4C38-A90D-5E909BD9EDD0}.Release|x86.ActiveCfg = Release|Any CPU {069365DB-1916-4C38-A90D-5E909BD9EDD0}.Release|x86.Build.0 = Release|Any CPU + {BE44B4EB-9AB9-4D81-A9BF-5CF2832BEEE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BE44B4EB-9AB9-4D81-A9BF-5CF2832BEEE5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BE44B4EB-9AB9-4D81-A9BF-5CF2832BEEE5}.Debug|x64.ActiveCfg = Debug|Any CPU + {BE44B4EB-9AB9-4D81-A9BF-5CF2832BEEE5}.Debug|x64.Build.0 = Debug|Any CPU + {BE44B4EB-9AB9-4D81-A9BF-5CF2832BEEE5}.Debug|x86.ActiveCfg = Debug|Any CPU + {BE44B4EB-9AB9-4D81-A9BF-5CF2832BEEE5}.Debug|x86.Build.0 = Debug|Any CPU + {BE44B4EB-9AB9-4D81-A9BF-5CF2832BEEE5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BE44B4EB-9AB9-4D81-A9BF-5CF2832BEEE5}.Release|Any CPU.Build.0 = Release|Any CPU + {BE44B4EB-9AB9-4D81-A9BF-5CF2832BEEE5}.Release|x64.ActiveCfg = Release|Any CPU + {BE44B4EB-9AB9-4D81-A9BF-5CF2832BEEE5}.Release|x64.Build.0 = Release|Any CPU + {BE44B4EB-9AB9-4D81-A9BF-5CF2832BEEE5}.Release|x86.ActiveCfg = Release|Any CPU + {BE44B4EB-9AB9-4D81-A9BF-5CF2832BEEE5}.Release|x86.Build.0 = Release|Any CPU + {434119EA-2FFC-4433-9B8E-1E6D94006413}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {434119EA-2FFC-4433-9B8E-1E6D94006413}.Debug|Any CPU.Build.0 = Debug|Any CPU + {434119EA-2FFC-4433-9B8E-1E6D94006413}.Debug|x64.ActiveCfg = Debug|Any CPU + {434119EA-2FFC-4433-9B8E-1E6D94006413}.Debug|x64.Build.0 = Debug|Any CPU + {434119EA-2FFC-4433-9B8E-1E6D94006413}.Debug|x86.ActiveCfg = Debug|Any CPU + {434119EA-2FFC-4433-9B8E-1E6D94006413}.Debug|x86.Build.0 = Debug|Any CPU + {434119EA-2FFC-4433-9B8E-1E6D94006413}.Release|Any CPU.ActiveCfg = Release|Any CPU + {434119EA-2FFC-4433-9B8E-1E6D94006413}.Release|Any CPU.Build.0 = Release|Any CPU + {434119EA-2FFC-4433-9B8E-1E6D94006413}.Release|x64.ActiveCfg = Release|Any CPU + {434119EA-2FFC-4433-9B8E-1E6D94006413}.Release|x64.Build.0 = Release|Any CPU + {434119EA-2FFC-4433-9B8E-1E6D94006413}.Release|x86.ActiveCfg = Release|Any CPU + {434119EA-2FFC-4433-9B8E-1E6D94006413}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -340,6 +368,8 @@ Global {92CA82EB-E558-44E7-9185-6FF8B8299C2A} = {D4E5F6A7-B8C9-4012-3456-789ABCDEF012} {02DE69CD-19E6-43C0-8916-DB98E5B5CA89} = {A9B8C7D6-E5F4-4321-ABCD-FEDCBA987654} {069365DB-1916-4C38-A90D-5E909BD9EDD0} = {A9B8C7D6-E5F4-4321-ABCD-FEDCBA987654} + {BE44B4EB-9AB9-4D81-A9BF-5CF2832BEEE5} = {0FE6558F-2157-47F2-A835-558416CE0E2B} + {434119EA-2FFC-4433-9B8E-1E6D94006413} = {0FE6558F-2157-47F2-A835-558416CE0E2B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6246A67B-299E-4E64-8DBE-1A66771E7C67}