Skip to content

Commit 2ea385d

Browse files
Refactoring of email messaging infrastructure (#6505)
1 parent ac13c9f commit 2ea385d

126 files changed

Lines changed: 10535 additions & 2453 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: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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+
namespace NuGetGallery.Authentication
5+
{
6+
public class CredentialTypeInfo
7+
{
8+
public CredentialTypeInfo(string type, bool isApiKey, string description)
9+
{
10+
Type = type;
11+
IsApiKey = isApiKey;
12+
Description = description;
13+
}
14+
15+
public string Type { get; }
16+
public bool IsApiKey { get; }
17+
public string Description { get; }
18+
}
19+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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.Net.Mail;
6+
7+
namespace NuGetGallery.Infrastructure.Mail
8+
{
9+
public abstract class ConfirmationEmailBuilder : MarkdownEmailBuilder
10+
{
11+
protected readonly IMessageServiceConfiguration Configuration;
12+
protected readonly string RawConfirmationUrl;
13+
protected readonly bool IsOrganization;
14+
15+
protected ConfirmationEmailBuilder(
16+
IMessageServiceConfiguration configuration,
17+
User user,
18+
string confirmationUrl)
19+
{
20+
Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
21+
User = user ?? throw new ArgumentNullException(nameof(user));
22+
IsOrganization = user is Organization;
23+
RawConfirmationUrl = confirmationUrl ?? throw new ArgumentNullException(nameof(confirmationUrl));
24+
ConfirmationUrl = EscapeLinkForMarkdown(confirmationUrl);
25+
}
26+
27+
public override MailAddress Sender => Configuration.GalleryNoReplyAddress;
28+
29+
public User User { get; }
30+
public string ConfirmationUrl { get; }
31+
}
32+
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
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.ObjectModel;
6+
using System.Globalization;
7+
using System.Linq;
8+
using System.Net.Mail;
9+
using System.Threading.Tasks;
10+
using AnglicanGeek.MarkdownMailer;
11+
12+
namespace NuGetGallery.Infrastructure.Mail
13+
{
14+
public class CoreMarkdownMessageService : IMessageService
15+
{
16+
private static readonly ReadOnlyCollection<TimeSpan> RetryDelays = Array.AsReadOnly(new[] {
17+
TimeSpan.FromSeconds(0.1),
18+
TimeSpan.FromSeconds(1),
19+
TimeSpan.FromSeconds(10)
20+
});
21+
22+
public CoreMarkdownMessageService(
23+
IMailSender mailSender,
24+
IMessageServiceConfiguration configuration)
25+
{
26+
MailSender = mailSender ?? throw new ArgumentNullException(nameof(mailSender));
27+
Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
28+
}
29+
30+
public IMailSender MailSender { get; protected set; }
31+
public IMessageServiceConfiguration Configuration { get; protected set; }
32+
33+
public async Task SendMessageAsync(IEmailBuilder emailBuilder, bool copySender = false, bool discloseSenderAddress = false)
34+
{
35+
if (emailBuilder == null)
36+
{
37+
throw new ArgumentNullException(nameof(emailBuilder));
38+
}
39+
40+
using (var email = CreateMailMessage(emailBuilder))
41+
{
42+
await SendMessageInternalAsync(email, copySender, discloseSenderAddress);
43+
}
44+
}
45+
46+
protected virtual async Task SendMessageInternalAsync(MailMessage mailMessage, bool copySender = false, bool discloseSenderAddress = false)
47+
{
48+
if (!mailMessage.To.Any())
49+
{
50+
return;
51+
}
52+
53+
var attempt = 0;
54+
var success = false;
55+
while (!success)
56+
{
57+
try
58+
{
59+
await AttemptSendMessageAsync(mailMessage, attempt + 1);
60+
success = true;
61+
}
62+
catch (SmtpException)
63+
{
64+
if (attempt < RetryDelays.Count)
65+
{
66+
await Task.Delay(RetryDelays[attempt]);
67+
attempt++;
68+
}
69+
else
70+
{
71+
throw;
72+
}
73+
}
74+
}
75+
76+
if (copySender && !discloseSenderAddress)
77+
{
78+
await SendMessageToSenderAsync(mailMessage);
79+
}
80+
}
81+
82+
protected virtual Task AttemptSendMessageAsync(MailMessage mailMessage, int attemptNumber)
83+
{
84+
// AnglicanGeek.MarkdownMailer doesn't have an async overload
85+
MailSender.Send(mailMessage);
86+
return Task.CompletedTask;
87+
}
88+
89+
protected static MailMessage CreateMailMessage(IEmailBuilder emailBuilder)
90+
{
91+
if (emailBuilder == null)
92+
{
93+
throw new ArgumentNullException(nameof(emailBuilder));
94+
}
95+
96+
var mailMessage = new MailMessage();
97+
mailMessage.From = emailBuilder.Sender;
98+
mailMessage.Subject = emailBuilder.GetSubject();
99+
mailMessage.Body = emailBuilder.GetBody(EmailFormat.Markdown);
100+
101+
var recipients = emailBuilder.GetRecipients();
102+
foreach (var toAddress in recipients.To)
103+
{
104+
mailMessage.To.Add(toAddress);
105+
}
106+
107+
foreach (var ccAddress in recipients.CC)
108+
{
109+
mailMessage.CC.Add(ccAddress);
110+
}
111+
112+
foreach (var bccAddress in recipients.Bcc)
113+
{
114+
mailMessage.Bcc.Add(bccAddress);
115+
}
116+
117+
foreach (var replyToAddress in recipients.ReplyTo)
118+
{
119+
mailMessage.ReplyToList.Add(replyToAddress);
120+
}
121+
122+
return mailMessage;
123+
}
124+
125+
private async Task SendMessageToSenderAsync(MailMessage mailMessage)
126+
{
127+
using (var senderCopy = new MailMessage(
128+
Configuration.GalleryOwner,
129+
mailMessage.ReplyToList.First()))
130+
{
131+
senderCopy.Subject = mailMessage.Subject + " [Sender Copy]";
132+
senderCopy.Body = string.Format(
133+
CultureInfo.CurrentCulture,
134+
"You sent the following message via {0}: {1}{1}{2}",
135+
Configuration.GalleryOwner.DisplayName,
136+
Environment.NewLine,
137+
mailMessage.Body);
138+
senderCopy.ReplyToList.Add(mailMessage.ReplyToList.First());
139+
await SendMessageInternalAsync(senderCopy);
140+
}
141+
}
142+
}
143+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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.Net.Mail;
6+
using System.Web;
7+
8+
namespace NuGetGallery.Infrastructure.Mail
9+
{
10+
/// <summary>
11+
/// Abstract base class for building email messages.
12+
/// </summary>
13+
public abstract class EmailBuilder : IEmailBuilder
14+
{
15+
public abstract MailAddress Sender { get; }
16+
17+
public string GetBody(EmailFormat format)
18+
{
19+
switch (format)
20+
{
21+
case EmailFormat.PlainText:
22+
return GetPlainTextBody();
23+
case EmailFormat.Markdown:
24+
return GetMarkdownBody();
25+
default:
26+
throw new ArgumentOutOfRangeException(nameof(format));
27+
}
28+
}
29+
30+
public abstract IEmailRecipients GetRecipients();
31+
32+
public abstract string GetSubject();
33+
34+
protected abstract string GetPlainTextBody();
35+
protected abstract string GetMarkdownBody();
36+
37+
/// <summary>
38+
/// Markdown sees the underscore as italics indicator, so underscores are stripped in the message.
39+
/// This prevents cut and pasting of the address or the use of text only email readers.
40+
/// </summary>
41+
/// <param name="encodedUrl">The encoded Url</param>
42+
/// <returns>Returns a Markdown-friendly url by escaping underscore characters.</returns>
43+
protected string EscapeLinkForMarkdown(string encodedUrl)
44+
{
45+
if (encodedUrl == null)
46+
{
47+
throw new ArgumentNullException(nameof(encodedUrl));
48+
}
49+
50+
return HttpUtility.UrlDecode(encodedUrl).Replace("_", "\\_");
51+
}
52+
}
53+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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+
namespace NuGetGallery.Infrastructure.Mail
5+
{
6+
/// <summary>
7+
/// Defines the formatting to be used by <see cref="EmailBuilder"/> for creating email messages.
8+
/// </summary>
9+
public enum EmailFormat
10+
{
11+
/// <summary>
12+
/// Indicates that <see cref="EmailBuilder"/> will create plain-text email messages, without markup.
13+
/// Used as a fallback by email clients that don't support other formats.
14+
/// </summary>
15+
PlainText,
16+
17+
/// <summary>
18+
/// Indicates that <see cref="EmailBuilder"/> will create email messages using Markdown formatting,
19+
/// which will be rendered as HTML by email clients.
20+
/// </summary>
21+
Markdown
22+
}
23+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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.Net.Mail;
8+
9+
namespace NuGetGallery.Infrastructure.Mail
10+
{
11+
public class EmailRecipients : IEmailRecipients
12+
{
13+
public EmailRecipients(
14+
IReadOnlyList<MailAddress> to,
15+
IReadOnlyList<MailAddress> cc = null,
16+
IReadOnlyList<MailAddress> bcc = null,
17+
IReadOnlyList<MailAddress> replyTo = null)
18+
{
19+
To = to ?? throw new ArgumentNullException(nameof(to));
20+
CC = cc ?? new List<MailAddress>();
21+
Bcc = bcc ?? new List<MailAddress>();
22+
ReplyTo = replyTo ?? new List<MailAddress>();
23+
}
24+
25+
public static IEmailRecipients None = new EmailRecipients(to: Array.Empty<MailAddress>());
26+
27+
public IReadOnlyList<MailAddress> To { get; }
28+
29+
public IReadOnlyList<MailAddress> CC { get; }
30+
31+
public IReadOnlyList<MailAddress> Bcc { get; }
32+
33+
public IReadOnlyList<MailAddress> ReplyTo { get; }
34+
35+
public static IReadOnlyList<MailAddress> GetAllOwners(PackageRegistration packageRegistration, bool requireEmailAllowed)
36+
{
37+
if (packageRegistration == null)
38+
{
39+
throw new ArgumentNullException(nameof(packageRegistration));
40+
}
41+
42+
var recipients = new List<MailAddress>();
43+
var owners = requireEmailAllowed
44+
? packageRegistration.Owners.Where(o => o.EmailAllowed)
45+
: packageRegistration.Owners;
46+
47+
foreach (var owner in owners)
48+
{
49+
recipients.Add(owner.ToMailAddress());
50+
}
51+
52+
return recipients;
53+
}
54+
55+
public static IReadOnlyList<MailAddress> GetOwnersSubscribedToPackagePushedNotification(PackageRegistration packageRegistration)
56+
{
57+
if (packageRegistration == null)
58+
{
59+
throw new ArgumentNullException(nameof(packageRegistration));
60+
}
61+
62+
var recipients = new List<MailAddress>();
63+
foreach (var owner in packageRegistration.Owners.Where(o => o.NotifyPackagePushed))
64+
{
65+
recipients.Add(owner.ToMailAddress());
66+
}
67+
68+
return recipients;
69+
}
70+
}
71+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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.Net.Mail;
5+
6+
namespace NuGetGallery.Infrastructure.Mail
7+
{
8+
/// <summary>
9+
/// Represents an email message builder.
10+
/// </summary>
11+
public interface IEmailBuilder
12+
{
13+
/// <summary>
14+
/// The sender of the email message.
15+
/// </summary>
16+
MailAddress Sender { get; }
17+
18+
/// <summary>
19+
/// Retrieve the email message body in the requested <paramref name="format"/>.
20+
/// </summary>
21+
/// <param name="format">The requested markup format for the email body.</param>
22+
string GetBody(EmailFormat format);
23+
24+
/// <summary>
25+
/// Gets the email message subject.
26+
/// </summary>
27+
string GetSubject();
28+
29+
/// <summary>
30+
/// Gets the email recipients.
31+
/// </summary>
32+
IEmailRecipients GetRecipients();
33+
}
34+
}

0 commit comments

Comments
 (0)