Skip to content

Commit 9411c69

Browse files
Emails should not be sent synchronously (#6555)
1 parent 88588f8 commit 9411c69

53 files changed

Lines changed: 965 additions & 87 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Threading.Tasks;
8+
using NuGet.Services.Messaging;
9+
10+
namespace NuGetGallery.Infrastructure.Mail
11+
{
12+
public class AsynchronousEmailMessageService : IMessageService
13+
{
14+
private readonly IMessageServiceConfiguration _configuration;
15+
private readonly IEmailMessageEnqueuer _emailMessageEnqueuer;
16+
17+
public AsynchronousEmailMessageService(
18+
IMessageServiceConfiguration configuration,
19+
IEmailMessageEnqueuer emailMessageEnqueuer)
20+
{
21+
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
22+
_emailMessageEnqueuer = emailMessageEnqueuer ?? throw new ArgumentNullException(nameof(emailMessageEnqueuer));
23+
}
24+
25+
public Task SendMessageAsync(
26+
IEmailBuilder emailBuilder,
27+
bool copySender = false,
28+
bool discloseSenderAddress = false)
29+
{
30+
if (emailBuilder == null)
31+
{
32+
throw new ArgumentNullException(nameof(emailBuilder));
33+
}
34+
35+
var message = CreateMessage(
36+
emailBuilder,
37+
copySender,
38+
discloseSenderAddress);
39+
40+
return EnqueueMessageAsync(message);
41+
}
42+
43+
private static EmailMessageData CreateMessage(
44+
IEmailBuilder emailBuilder,
45+
bool copySender = false,
46+
bool discloseSenderAddress = false)
47+
{
48+
var recipients = emailBuilder.GetRecipients();
49+
50+
if (recipients == EmailRecipients.None)
51+
{
52+
// Optimization: no need to construct message body when no recipients.
53+
return null;
54+
}
55+
56+
if (emailBuilder.Sender == null)
57+
{
58+
throw new ArgumentException(
59+
$"No sender defined for message of type '{emailBuilder.GetType()}'.",
60+
nameof(emailBuilder.Sender));
61+
}
62+
63+
return new EmailMessageData(
64+
emailBuilder.GetSubject(),
65+
emailBuilder.GetBody(EmailFormat.PlainText),
66+
emailBuilder.GetBody(EmailFormat.Html),
67+
emailBuilder.Sender.Address,
68+
to: recipients.To.Select(e => e.Address).ToList(),
69+
cc: GenerateCC(
70+
emailBuilder.Sender.Address,
71+
recipients.CC.Select(e => e.Address).ToList(),
72+
copySender,
73+
discloseSenderAddress),
74+
bcc: recipients.Bcc.Select(e => e.Address).ToList(),
75+
replyTo: recipients.ReplyTo.Select(e => e.Address).ToList(),
76+
messageTrackingId: Guid.NewGuid());
77+
}
78+
79+
private static IReadOnlyList<string> GenerateCC(
80+
string fromAddress,
81+
IReadOnlyList<string> cc, bool copySender,
82+
bool discloseSenderAddress)
83+
{
84+
var ccList = new List<string>();
85+
if (cc != null)
86+
{
87+
ccList.AddRange(cc);
88+
}
89+
90+
if (copySender && discloseSenderAddress && !ccList.Contains(fromAddress))
91+
{
92+
ccList.Add(fromAddress);
93+
}
94+
95+
return ccList;
96+
}
97+
98+
private Task EnqueueMessageAsync(EmailMessageData message)
99+
{
100+
if (message == null || !message.To.Any())
101+
{
102+
return Task.CompletedTask;
103+
}
104+
105+
if (string.IsNullOrEmpty(message.HtmlBody)
106+
&& string.IsNullOrEmpty(message.PlainTextBody))
107+
{
108+
throw new ArgumentException(
109+
"No message body defined. Both plain-text and html bodies are empty.",
110+
nameof(message));
111+
}
112+
113+
return _emailMessageEnqueuer.SendEmailMessageAsync(message);
114+
}
115+
}
116+
}

src/NuGetGallery.Core/Infrastructure/mail/EmailBuilder.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ public string GetBody(EmailFormat format)
2222
return GetPlainTextBody();
2323
case EmailFormat.Markdown:
2424
return GetMarkdownBody();
25+
case EmailFormat.Html:
26+
return GetHtmlBody();
2527
default:
2628
throw new ArgumentOutOfRangeException(nameof(format));
2729
}
@@ -33,6 +35,7 @@ public string GetBody(EmailFormat format)
3335

3436
protected abstract string GetPlainTextBody();
3537
protected abstract string GetMarkdownBody();
38+
protected abstract string GetHtmlBody();
3639

3740
/// <summary>
3841
/// Markdown sees the underscore as italics indicator, so underscores are stripped in the message.

src/NuGetGallery.Core/Infrastructure/mail/EmailFormat.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ public enum EmailFormat
1818
/// Indicates that <see cref="EmailBuilder"/> will create email messages using Markdown formatting,
1919
/// which will be rendered as HTML by email clients.
2020
/// </summary>
21-
Markdown
21+
Markdown,
22+
23+
/// <summary>
24+
/// Indicates that <see cref="EmailBuilder"/> will create email messages using HTML formatting.
25+
/// </summary>
26+
Html
2227
}
2328
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
6+
namespace NuGetGallery.Infrastructure.Mail
7+
{
8+
/// <summary>
9+
/// Utility class to construct common email message footers.
10+
/// </summary>
11+
public static class EmailMessageFooter
12+
{
13+
public static string ForPackageOwnerNotifications(EmailFormat format, string galleryOwnerDisplayName, string emailSettingsUrl)
14+
{
15+
return ForReason(
16+
format,
17+
galleryOwnerDisplayName,
18+
emailSettingsUrl,
19+
"To stop receiving emails as an owner of this package");
20+
}
21+
22+
public static string ForContactOwnerNotifications(EmailFormat format, string galleryOwnerDisplayName, string emailSettingsUrl)
23+
{
24+
return ForReason(
25+
format,
26+
galleryOwnerDisplayName,
27+
emailSettingsUrl,
28+
"To stop receiving contact emails as an owner of this package");
29+
}
30+
31+
private static string ForReason(EmailFormat format, string galleryOwnerDisplayName, string emailSettingsUrl, string reason)
32+
{
33+
switch (format)
34+
{
35+
case EmailFormat.PlainText:
36+
// Hyperlinks within an HTML emphasis tag are not supported by the Plain Text renderer in Markdig.
37+
return $@"
38+
39+
-----------------------------------------------
40+
{reason}, sign in to the {galleryOwnerDisplayName} and
41+
change your email notification settings ({emailSettingsUrl}).";
42+
case EmailFormat.Html:
43+
// Hyperlinks within an HTML emphasis tag are not supported by the HTML renderer in Markdig.
44+
return $@"<hr />
45+
<em style=""font-size: 0.8em;"">
46+
{reason}, sign in to the {galleryOwnerDisplayName} and
47+
<a href=""{emailSettingsUrl}"">change your email notification settings</a>.
48+
</em>";
49+
case EmailFormat.Markdown:
50+
return $@"
51+
52+
-----------------------------------------------
53+
<em style=""font-size: 0.8em;"">
54+
{reason}, sign in to the {galleryOwnerDisplayName} and
55+
[change your email notification settings]({emailSettingsUrl}).
56+
</em>";
57+
default:
58+
throw new ArgumentOutOfRangeException(nameof(format));
59+
}
60+
}
61+
}
62+
}

src/NuGetGallery.Core/Infrastructure/mail/MarkdownEmailBuilder.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ protected override string GetPlainTextBody()
1515
{
1616
var markdown = GetMarkdownBody();
1717

18+
return ToPlainText(markdown);
19+
}
20+
21+
protected string ToPlainText(string markdown)
22+
{
1823
var writer = new StringWriter();
1924
var pipeline = new MarkdownPipelineBuilder().Build();
2025

@@ -29,5 +34,10 @@ protected override string GetPlainTextBody()
2934

3035
return writer.ToString();
3136
}
37+
38+
protected override string GetHtmlBody()
39+
{
40+
return Markdown.ToHtml(GetMarkdownBody());
41+
}
3242
}
3343
}

src/NuGetGallery.Core/Infrastructure/mail/Messages/PackageAddedMessage.cs

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.Linq;
77
using System.Net.Mail;
8+
using Markdig;
89

910
namespace NuGetGallery.Infrastructure.Mail.Messages
1011
{
@@ -58,38 +59,57 @@ public override string GetSubject()
5859

5960
protected override string GetMarkdownBody()
6061
{
61-
var warningMessagesPlaceholder = string.Empty;
62-
if (_hasWarnings)
62+
return GetBodyInternal(EmailFormat.Markdown);
63+
}
64+
65+
protected override string GetPlainTextBody()
66+
{
67+
return GetBodyInternal(EmailFormat.PlainText);
68+
}
69+
70+
protected override string GetHtmlBody()
71+
{
72+
return GetBodyInternal(EmailFormat.Html);
73+
}
74+
75+
private string GetBodyInternal(EmailFormat format)
76+
{
77+
var warningMessages = GetWarningMessages();
78+
79+
var markdown = $@"The package [{Package.PackageRegistration.Id} {Package.Version}]({_packageUrl}) was recently published on {_configuration.GalleryOwner.DisplayName} by {Package.User.Username}. If this was not intended, please [contact support]({_packageSupportUrl}).";
80+
81+
if (!string.IsNullOrEmpty(warningMessages))
6382
{
64-
warningMessagesPlaceholder = Environment.NewLine + string.Join(Environment.NewLine, _warningMessages);
83+
markdown += warningMessages;
6584
}
6685

67-
return $@"The package [{Package.PackageRegistration.Id} {Package.Version}]({_packageUrl}) was recently published on {_configuration.GalleryOwner.DisplayName} by {Package.User.Username}. If this was not intended, please [contact support]({_packageSupportUrl}).
68-
{warningMessagesPlaceholder}
86+
string body;
87+
switch (format)
88+
{
89+
case EmailFormat.PlainText:
90+
body = ToPlainText(markdown);
91+
break;
92+
case EmailFormat.Markdown:
93+
body = markdown;
94+
break;
95+
case EmailFormat.Html:
96+
body = Markdown.ToHtml(markdown);
97+
break;
98+
default:
99+
throw new ArgumentOutOfRangeException(nameof(format));
100+
}
69101

70-
-----------------------------------------------
71-
<em style=""font-size: 0.8em;"">
72-
To stop receiving emails as an owner of this package, sign in to the {_configuration.GalleryOwner.DisplayName} and
73-
[change your email notification settings]({_emailSettingsUrl}).
74-
</em>";
102+
return body + EmailMessageFooter.ForPackageOwnerNotifications(format, _configuration.GalleryOwner.DisplayName, _emailSettingsUrl);
75103
}
76104

77-
protected override string GetPlainTextBody()
105+
private string GetWarningMessages()
78106
{
79-
// The HTML emphasis tag is not supported by the Plain Text renderer in Markdig.
80-
// Manually overriding this one.
81107
var warningMessagesPlaceholder = string.Empty;
82108
if (_hasWarnings)
83109
{
84-
warningMessagesPlaceholder = Environment.NewLine + string.Join(Environment.NewLine, _warningMessages);
110+
warningMessagesPlaceholder = Environment.NewLine + Environment.NewLine + string.Join(Environment.NewLine, _warningMessages);
85111
}
86-
87-
return $@"The package {Package.PackageRegistration.Id} {Package.Version} ({_packageUrl}) was recently published on {_configuration.GalleryOwner.DisplayName} by {Package.User.Username}. If this was not intended, please contact support ({_packageSupportUrl}).
88-
{warningMessagesPlaceholder}
89-
90-
-----------------------------------------------
91-
To stop receiving emails as an owner of this package, sign in to the {_configuration.GalleryOwner.DisplayName} and
92-
change your email notification settings ({_emailSettingsUrl}).";
112+
return warningMessagesPlaceholder;
93113
}
94114
}
95115
}

0 commit comments

Comments
 (0)