Open ticticboooom opened 4 years ago
I am happy to write this package and test it
@KyleBCox I'm not an expert but have been sending emails from a AWS Lambda (on a VPC) through to SES and have had a couple of problems that an implementation might want to be aware of. Sorry if this is a bit of ramble. Although, first, I haven't found anywhere in the documentation on AWS that suggests a preference for SES API over smtp for access. Both are documented.
I have, however, had some problems with the resulting emails via smtp (ie SmtpSender) and having also written a SES API wrapper get a different result. This is what I'll document (this a couple of hours work and hopefully have a full enough picture not to be misleading). My sense is also you need to decide on your credentials strategy as a major factor. Smtp requires SES credentials whereas SES API uses AWS/region credentials. I added code at the bottom to demonstrate this.
Using SmtpSender:
Using SES API (see naive implementation below):
/// <summary>
/// Implementation (wrapper) for the AWS Simple Email Service.
/// </summary>
/// <remarks>
/// Ported from <see cref="FluentEmail.Smtp"/> decompiled via JetBrains Rider
/// </remarks>
public class AwsSesSender : ISender
{
private readonly Func<IAmazonSimpleEmailService> _clientFactory;
public AwsSesSender(Func<IAmazonSimpleEmailService> clientFactory)
{
_clientFactory = clientFactory;
}
public SendResponse Send(IFluentEmail email, CancellationToken? token = null)
{
return SendAsync(email, token).GetAwaiter().GetResult();
}
public async Task<SendResponse> SendAsync(IFluentEmail email, CancellationToken? token = null)
{
var response = new SendResponse();
var mailMessage = CreateMailMessage(email);
if ((token.HasValue ? (token.GetValueOrDefault().IsCancellationRequested ? 1 : 0) : 0) != 0)
{
response.ErrorMessages.Add("Message was cancelled by cancellation token.");
return response;
}
using (var client = _clientFactory())
{
await client.SendEmailAsync(mailMessage);
}
return response;
}
/// <summary>
/// Maps and constructs the payload
/// </summary>
/// <remarks>
/// see https://github.com/awsdocs/amazon-ses-developer-guide/blob/master/doc-source/send-using-sdk-net.md
/// </remarks>
private SendEmailRequest CreateMailMessage(IFluentEmail email)
{
EmailData data = email.Data;
var sendRequest = new SendEmailRequest
{
Source = data.FromAddress.EmailAddress,
Destination = new Destination
{
ToAddresses = data.ToAddresses.Select(x => x.EmailAddress).ToList(),
BccAddresses = data.BccAddresses.Select(x => x.EmailAddress).ToList(),
CcAddresses = data.CcAddresses.Select(x => x.EmailAddress).ToList(),
},
Message = new Message
{
Subject = new Content(data.Subject),
Body = new Body
{
Html = new Content
{
Charset = "UTF-8",
Data = data.Body
},
Text = new Content
{
Charset = "UTF-8",
Data = data.PlaintextAlternativeBody.IfNullOrWhitespace("")
},
}
}
};
return sendRequest;
}
}
Here an extract of some of the code that would go in way to reproduce what I did:
// injected set of credentials and configuration
var config = Get<EmailConfig>();
var template = @"<div>Some text {{ text }} in html</div>";
var model = new {text = "XX"};
var emailer = new FluentEmail.Core.Email(
new HandlebarsRenderer(Handlebars.Create()),
new SmtpSender(() => new SmtpClient(config.Host, config.Port)
{
EnableSsl = true,
Credentials = new NetworkCredential(config.Username, config.Password)
}),
config.DefaultFromEmail,
config.DefaultFromName);
await emailer
.To("local-test@example.com")
.Subject("Test smtp to SES with plain text NOT SET")
.UsingTemplate(template, model)
.SendAsync();
await emailer
.To("local-test@example.com")
.Subject("Test smtp to SES with plain text (results in base64 encoded html")
.PlaintextAlternativeBody(" ")
.UsingTemplate(template, model)
.SendAsync();
await new FluentEmail.Core.Email(
new HandlebarsRenderer(Handlebars.Create()),
new AwsSesSender(() =>
new AmazonSimpleEmailServiceClient(RegionEndpoint.USEast1)),
config.DefaultFromEmail,
config.DefaultFromName).To("local-test@example.com")
.Subject("Test through SES API")
.UsingTemplate(template, model).SendAsync();
@KyleBCox @toddb did anything get implemented here ?
@Simonl9l I haven't been back here since I wrote the comment—so I don't know. Sorry.
However, I did a quick look at my code noticed that I have changed it slightly to include a sourceArn
that I am injecting from configuration (my tests inject null
). Apologies that I haven't documented why I needed the sourceArn :-(
/// <summary>
/// Implementation (wrapper) for the AWS Simple Email Service.
/// </summary>
/// <remarks>
/// Ported from <see cref="FluentEmail.Smtp"/> decompiled via JetBrains Rider
/// </remarks>
public class AwsSesSender : ISender
{
private readonly Func<IAmazonSimpleEmailService> _clientFactory;
private readonly string _sourceArn;
public AwsSesSender(Func<IAmazonSimpleEmailService> clientFactory, string sourceArn)
{
_clientFactory = clientFactory;
_sourceArn = sourceArn;
}
public AwsSesSender(Func<IAmazonSimpleEmailService> clientFactory)
{
_clientFactory = clientFactory;
}
public SendResponse Send(IFluentEmail email, CancellationToken? token = null)
{
return SendAsync(email, token).GetAwaiter().GetResult();
}
public async Task<SendResponse> SendAsync(IFluentEmail email, CancellationToken? token = null)
{
var response = new SendResponse();
var mailMessage = CreateMailMessage(email);
if ((token.HasValue ? (token.GetValueOrDefault().IsCancellationRequested ? 1 : 0) : 0) != 0)
{
response.ErrorMessages.Add("Message was cancelled by cancellation token.");
return response;
}
using (var client = _clientFactory())
{
await client.SendEmailAsync(mailMessage);
}
return response;
}
private string FormatEmail(Address address)
{
return $"{address.Name} <{address.EmailAddress}>";
}
/// <summary>
/// Maps and constructs the payload
/// </summary>
/// <remarks>
/// see https://github.com/awsdocs/amazon-ses-developer-guide/blob/master/doc-source/send-using-sdk-net.md
/// </remarks>
private SendEmailRequest CreateMailMessage(IFluentEmail email)
{
EmailData data = email.Data;
var sendRequest = new SendEmailRequest
{
// if available use the source arn for authorisation
SourceArn = _sourceArn,
//
Source = FormatEmail(data.FromAddress),
Destination = new Destination
{
// TODO: have email addresses that are RFC standard with names
ToAddresses = data.ToAddresses.Select(x => x.EmailAddress).ToList(),
BccAddresses = data.BccAddresses.Select(x => x.EmailAddress).ToList(),
CcAddresses = data.CcAddresses.Select(x => x.EmailAddress).ToList(),
},
Message = new Message
{
Subject = new Content(data.Subject),
Body = new Body
{
Html = new Content
{
Charset = "UTF-8",
Data = data.Body
},
Text = new Content
{
Charset = "UTF-8",
Data = data.PlaintextAlternativeBody.IfNullOrWhitespace("")
},
}
}
};
return sendRequest;
}
}
Hi all,
If you have something tested and working open a PR and I will try get it merged in.
Luke
0-60 here is a tad tough...not even familiar with FluentEmail...yet...
My DotNet process is running "as" an IAM, so assume not to need a SourceARN is that is somehow denoting the identity in the SES policy to send the email..
OK, this more or less works... given @toddb's code above:
namespace Services.Email
{
public static class EmailServiceExtensions
{
public static IServiceCollection AddEmailServices(this IServiceCollection services, Action<EmailSettings> options = null)
{
services
.Configure(options)
.AddFluentEmail("<default from email>")
.AddRazorRenderer()
.Services.Add(ServiceDescriptor.Scoped(x => (ISender) new AwsSesSender(() => new AmazonSimpleEmailServiceClient(RegionEndpoint.USWest2))));
return services;
}
}
}
and added the services.AddEmailServices
to my startup.cs
if this helps anyone else.
@lukencode do you have any recommendation on what's needed and supported by RazorLight to embed any css in the email, and add attachments etc.
Attachments need to be handled in the ISender implementation. There is an example in the SendGrid sender here: https://github.com/lukencode/FluentEmail/blob/4b9740bb1b1df9750f0d7043f6ca6c72bc39b86f/src/Senders/FluentEmail.SendGrid/SendGridSender.cs#L100
Not sure what sort of CSS embedding you want to do. The most common approach I have seen is using
There is currently an Smtp sender which will support AWS SES Smtp connections, however this is not the prefered way to send emails to the service.
Can there be an AWS SES API sender added to the list of sender packages. This can use the AWSSDK.SimpleEmail Nuget package to make the API Request.