I'm searching for an working C# example to send attachments with Amazon-SES.
After reading that Amazon-SES now supports sending attachments I was searching for an C# example but was unable to find one.
I think that using AWS SDK for .NET and MimeKit is very easy and clean solution. You can send e-mails with attachments via SES API (instead of SMTP).
You can write MimeMessage directly to MemoryStream and then use it with SES SendRawEmail:
using Amazon.SimpleEmail;
using Amazon.SimpleEmail.Model;
using Amazon;
using Amazon.Runtime;
using MimeKit;
private static BodyBuilder GetMessageBody()
{
var body = new BodyBuilder()
{
HtmlBody = #"<p>Amazon SES Test body</p>",
TextBody = "Amazon SES Test body",
};
body.Attachments.Add(#"c:\attachment.txt");
return body;
}
private static MimeMessage GetMessage()
{
var message = new MimeMessage();
message.From.Add(new MailboxAddress("Foo Bar", "foo#bar.com"));
message.To.Add(new MailboxAddress(string.Empty, "foobar#example.com"));
message.Subject = "Amazon SES Test";
message.Body = GetMessageBody().ToMessageBody();
return message;
}
private static MemoryStream GetMessageStream()
{
var stream = new MemoryStream();
GetMessage().WriteTo(stream);
return stream;
}
private void SendEmails()
{
var credentals = new BasicAWSCredentials("<aws-access-key>", "<aws-secret-key>");
using (var client = new AmazonSimpleEmailServiceClient(credentals, RegionEndpoint.EUWest1))
{
var sendRequest = new SendRawEmailRequest { RawMessage = new RawMessage(GetMessageStream()) };
try
{
var response = client.SendRawEmail(sendRequest);
Console.WriteLine("The email was sent successfully.");
}
catch (Exception e)
{
Console.WriteLine("The email was not sent.");
Console.WriteLine("Error message: " + e.Message);
}
}
}
public Boolean SendRawEmail(String from, String to, String cc, String Subject, String text, String html, String replyTo, string attachPath)
{
AlternateView plainView = AlternateView.CreateAlternateViewFromString(text, Encoding.UTF8, "text/plain");
AlternateView htmlView = AlternateView.CreateAlternateViewFromString(html, Encoding.UTF8, "text/html");
MailMessage mailMessage = new MailMessage();
mailMessage.From = new MailAddress(from);
List<String> toAddresses = to.Replace(", ", ",").Split(',').ToList();
foreach (String toAddress in toAddresses)
{
mailMessage.To.Add(new MailAddress(toAddress));
}
List<String> ccAddresses = cc.Replace(", ", ",").Split(',').Where(y => y != "").ToList();
foreach (String ccAddress in ccAddresses)
{
mailMessage.CC.Add(new MailAddress(ccAddress));
}
mailMessage.Subject = Subject;
mailMessage.SubjectEncoding = Encoding.UTF8;
if (replyTo != null)
{
mailMessage.ReplyToList.Add(new MailAddress(replyTo));
}
if (text != null)
{
mailMessage.AlternateViews.Add(plainView);
}
if (html != null)
{
mailMessage.AlternateViews.Add(htmlView);
}
if (attachPath.Trim() != "")
{
if (System.IO.File.Exists(attachPath))
{
System.Net.Mail.Attachment objAttach = new System.Net.Mail.Attachment(attachPath);
objAttach.ContentType = new ContentType("application/octet-stream");
System.Net.Mime.ContentDisposition disposition = objAttach.ContentDisposition;
disposition.DispositionType = "attachment";
disposition.CreationDate = System.IO.File.GetCreationTime(attachPath);
disposition.ModificationDate = System.IO.File.GetLastWriteTime(attachPath);
disposition.ReadDate = System.IO.File.GetLastAccessTime(attachPath);
mailMessage.Attachments.Add(objAttach);
}
}
RawMessage rawMessage = new RawMessage();
using (MemoryStream memoryStream = ConvertMailMessageToMemoryStream(mailMessage))
{
rawMessage.WithData(memoryStream);
}
SendRawEmailRequest request = new SendRawEmailRequest();
request.WithRawMessage(rawMessage);
request.WithDestinations(toAddresses);
request.WithSource(from);
AmazonSimpleEmailService ses = AWSClientFactory.CreateAmazonSimpleEmailServiceClient(ConfigurationManager.AppSettings.Get("AccessKeyId"), ConfigurationManager.AppSettings.Get("SecretKeyId"));
try
{
SendRawEmailResponse response = ses.SendRawEmail(request);
SendRawEmailResult result = response.SendRawEmailResult;
return true;
}
catch (AmazonSimpleEmailServiceException ex)
{
return false;
}
}
public static MemoryStream ConvertMailMessageToMemoryStream(MailMessage message)
{
Assembly assembly = typeof(SmtpClient).Assembly;
Type mailWriterType = assembly.GetType("System.Net.Mail.MailWriter");
MemoryStream fileStream = new MemoryStream();
ConstructorInfo mailWriterContructor = mailWriterType.GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, null, new[] { typeof(Stream) }, null);
object mailWriter = mailWriterContructor.Invoke(new object[] { fileStream });
MethodInfo sendMethod = typeof(MailMessage).GetMethod("Send", BindingFlags.Instance | BindingFlags.NonPublic);
sendMethod.Invoke(message, BindingFlags.Instance | BindingFlags.NonPublic, null, new[] { mailWriter, true }, null);
MethodInfo closeMethod = mailWriter.GetType().GetMethod("Close", BindingFlags.Instance | BindingFlags.NonPublic);
closeMethod.Invoke(mailWriter, BindingFlags.Instance | BindingFlags.NonPublic, null, new object[] { }, null);
return fileStream;
}
Found that here
Update: A method signature has changed in .NET 4.5, which breaks the above:
Getting System.Net.Mail.MailMessage as a MemoryStream in .NET 4.5 beta
This is a very simple implementation using MimeKit
using Amazon;
using Amazon.SimpleEmail;
using Amazon.SimpleEmail.Model;
using MimeKit;
using System.IO;
namespace SendEmailWithAttachments
{
class Program
{
static void Main(string[] args)
{
//Remember to enter your (AWSAccessKeyID, AWSSecretAccessKey) if not using and IAM User with credentials assigned to your instance and your RegionEndpoint
using (var client = new AmazonSimpleEmailServiceClient("YourAWSAccessKeyID", "YourAWSSecretAccessKey", RegionEndpoint.USEast1))
using (var messageStream = new MemoryStream())
{
var message = new MimeMessage();
var builder = new BodyBuilder() { TextBody = "Hello World" };
message.From.Add(new MailboxAddress("FROMADDRESS#TEST.COM"));
message.To.Add(new MailboxAddress("TOADDRESS#TEST.COM"));
message.Subject = "Hello World";
//I'm using the stream method, but you don't have to.
using (FileStream stream = File.Open(#"Attachment1.pdf", FileMode.Open)) builder.Attachments.Add("Attachment1.pdf", stream);
using (FileStream stream = File.Open(#"Attachment2.pdf", FileMode.Open)) builder.Attachments.Add("Attachment2.pdf", stream);
message.Body = builder.ToMessageBody();
message.WriteTo(messageStream);
var request = new SendRawEmailRequest()
{
RawMessage = new RawMessage() { Data = messageStream }
};
client.SendRawEmail(request);
}
}
}
}
I have the code in my repository https://github.com/gianluis90/amazon-send-email
You can setup IIS SMTP to relay through SES as well.
You need to install stunnel and set it up
Then you can just set the IIS SMTP Smart Host and some other options and it will relay your email through SES.
Instructions from above linked gist:
Instructions taken from Amazon's docs and modified as necessary.
1. Install stunnel:
Download from stunnel's download page
Run installer w/ default options, create self signed cert by answering questions
Open the c:\program files (x86)\stunnel\stunnel.conf file in notepad
Clear all the server configs (under Example SSL server mode services section, won't have a client = yes line)
Create a new client config:
[smtp-tls-wrapper]
accept = 127.0.0.1:2525
client = yes
connect = email-smtp.us-east-1.amazonaws.com:465
Start stunnel.exe and ensure no errors are reported (you will get a little systray icon)
If it succeeds you can optionally install as a service, by running stunnel.exe -install at command line (note this installs the service but doesn't start it, so start it)
Test the connection, at cmd line run telnet localhost 2525 and you should see an SMTP response from Amazon's server (if telnet isn't installed, add the feature in Server Manager / Features / Add Feature)
2. Configure IIS SMTP
Set Smart Host as [127.0.0.1] (include the brackets)
In the Outbound Connections section, set outgoing port to 2525 (like the stunnel.conf file)
In the Outbound Security section, set authentication information to your Amazon SMTP credentials, set it to basic authentication (NOTE: DO NOT CHECK THE TLS CHECKBOX)
I'm not sure if this is what you are looking for, but it's the only resource I've been able to find on the subject. I would love a better answer to this question, too.
http://docs.amazonwebservices.com/ses/latest/DeveloperGuide/
It states how to use it, but is very cryptic, at least to me.
Any better guides out there?
I also need help with this, but so far I've found you need to send a multipart MIME message, with the attachment encoded in base64.
I think you need to follow the instructions on this link. Amazon doesn't let you add attachments or other more complicated message types (iCalendar events). Essentially you need to handcraft the message body by string building and manipulation.
Currently I do this for iCalendar format emails on a legacy system. It's a bit of a pain in the ass, but if you read RFC 2822, it tells you pretty clearly what the format is. Special things to pay attention to:
Base64 encoding of the data
MIME types
Make sure your multipart boundaries match up (and are unique)
Finicky problems with the number of line breaks (\n) after certain lines
Best of luck. I don't know if there is an open library that will do this sort of thing for you in C#. If you can find one then try use it, because dealing with the intricacies of the RFC should have a medical notice for increased blood pressure and hair loss.
Related
I am created a report viewer using rdlc report system. but the problem is got, when I send this file by email then I found an error. My program is something like that:-
at first, I created a reporting process, which shows my "Order" database's all information. that information shows me as one file system on my desktop when I run my application. this file, I want to send by email. but when I trying to send this, I found many errors.
Here is my code:-
public async Task<IActionResult> CheckOut(Order11 anOrder)
{
//other code
//report process
string mimetype = "";
int extension = 1;
var path = $"{this._webHostEnvironment.WebRootPath}\\Reports\\Report2.rdlc";
Dictionary<string, string> parameters = new Dictionary<string, string>();
var products = _db.Order.ToList();
LocalReport localReport = new LocalReport(path);
localReport.AddDataSource("DataSet1", products);
var result = localReport.Execute(RenderType.Pdf, extension, parameters, mimetype);
var s= File(result.MainStream, "application/pdf");
//report process end
//Email Sender start
var email = anOrder.Email;
var message = new MimeMessage();
message.From.Add(new MailboxAddress("Ghatia Bazar",
"pri#gmail.com"));
message.To.Add(new MailboxAddress("pritom", email));
message.Subject = "Order Details";
message.Body = new TextPart("plain")
{
Text = "Hi,Thanks For Order.",
};
//add attach
MemoryStream memoryStream = new MemoryStream();
BodyBuilder bb = new BodyBuilder();
using (var wc = new WebClient())
{
bb.Attachments.Add("s",
wc.DownloadData("path"));
}
message.Body = bb.ToMessageBody();
//end attach
using (var client = new SmtpClient())
{
client.Connect("smtp.gmail.com", 587, false);
client.Authenticate("pri#gmail.com",
"MyPassword");
client.Send(message);
client.Disconnect(true);
}
//Email sender end
//other code
I also use bb.Attachments.Add("s", result.MainStream); instead of bb.Attachments.Add("s", wc.DownloadData("path")); when I use this, then I found an unexpected email. In this email's file, I found a lot of code. so now bb.Attachments.Add("s", wc.DownloadData("path")); I am using this process to attach a file. but here I found a different error.
Here is my output:-
How I will solve this problem.How I send my created file by email. I am still a beginner, please help.
From the code it looks like you are using AspNetCore.Reporting library for generating reports from RDLC files.
Code of this library is available on GitHub at https://github.com/amh1979/AspNetCore.Reporting
LocalReport class from this library has Execute method which return an instance of ReportResult class.
Code of both these classes is located at https://github.com/amh1979/AspNetCore.Reporting/blob/master/AspNetCore.Reporting/LocalReport.cs
ReportResult class has a Property MainStream which represents the content of the report as Byte Array.
Now, attachment to MimeMessage via BodyBuilder support various inputs for file contents such as Stream, byte[] etc.
With this knowledge, I think you can directly use ReportResult.MainStream to attach file to the BodyBuilder.
You can change your code as following to make it working.
var path = $"{this._webHostEnvironment.WebRootPath}\\Reports\\Report2.rdlc";
Dictionary<string, string> parameters = new Dictionary<string, string>();
var products = _db.Order.ToList();
LocalReport localReport = new LocalReport(path);
localReport.AddDataSource("DataSet1", products);
//Following line of code returns an instance of ReportResult class
var result = localReport.Execute(RenderType.Pdf, extension, parameters, mimetype);
var email = anOrder.Email;
var message = new MimeMessage();
message.From.Add(new MailboxAddress("Ghatia Bazar", "pri#gmail.com"));
message.To.Add(new MailboxAddress("pritom", email));
message.Subject = "Order Details";
message.Body = new TextPart("plain")
{
Text = "Hi,Thanks For Order.",
};
var bb = new BodyBuilder();
// Following line will attach the report as "MyFile.pdf".
// You can use filename of your choice.
bb.Attachments.Add("Myfile.pdf", result.MainStream);
message.Body = bb.ToMessageBody();
using (var client = new SmtpClient())
{
client.Connect("smtp.gmail.com", 587, false);
client.Authenticate("pri#gmail.com", "MyPassword");
client.Send(message);
client.Disconnect(true);
}
I hope this will help you resolve your issue.
TLDR; Which of the following (or otherwise) is most suitable for sending 300-500 emails quickly to an external mail server?
SmtpClient.SendAsync
SmtpClient.SendMailAsync
new Thread
Parallel.ForEach
I'm amending code for sending emails to now use an external sender (e.g. MailGun, SendGrid etc.) instead of HMailServer which is currently installed on the same server as the application. This is obviously introducing latency.
I've read the documentation for all of the above, but am struggling to fully understand the implications (particularly the gotcha's) of each. There seems to be very differing opinions about whether the above are suitable or not, particularly:
If I have to await each email, then is that not simulating the current sync approach?
I'm trying to use a single instance of the SmptClient object, for speed and efficiency
Based on extensive reading today, many examples I've seen are old, and may not use the latest .Net capabilities
I'd welcome input please from people who have actually achieved this successfully. Naturally I can go write code for each, but I'm looking for experienced people to guide me on the right path to begin with here.
My simplified (existing sync) code is as follows:
var sb = new StringBuilder();
using (var mc = new SmtpClient() {
Host = "127.0.0.1", // Current HMailServer installation - will be changed to external API
DeliveryMethod = SmtpDeliveryMethod.Network,
Port = 25,
UseDefaultCredentials = false,
Credentials = new NetworkCredential("Username", "Password")
})
{
foreach(var result in GetData())
{
using(var mm = new MailMessage())
{
mm.To.Add(new MailAddress(result.Email, result.FirstName + " " + result.Surname));
mm.Subject = "Your monthly report";
mm.From = new MailAddress("noreply#example.com");
mm.ReplyToList.Add(new MailAddress(result.Email));
// Email body constructed here for each individual recipient
mm.Body = sb.ToString();
sb.Clear();
mc.Send(mm);
}
}
}
For I/O-bound tasks like sending email, you do not want to use Parallel. This goes double if you're running on ASP.NET. Also, you don't want to use new Thread unless you're doing COM interop.
If you want to make it asynchronous, the easiest way is to keep everything the same and just call SendAsync instead of Send:
var sb = new StringBuilder();
using (var mc = new SmtpClient() {
Host = "127.0.0.1", // Current HMailServer installation - will be changed to external API
DeliveryMethod = SmtpDeliveryMethod.Network,
Port = 25,
UseDefaultCredentials = false,
Credentials = new NetworkCredential("Username", "Password")
})
{
foreach(var result in GetData())
{
using(var mm = new MailMessage())
{
mm.To.Add(new MailAddress(result.Email, result.FirstName + " " + result.Surname));
mm.Subject = "Your monthly report";
mm.From = new MailAddress("noreply#example.com");
mm.ReplyToList.Add(new MailAddress("admin#example.com"));
// Email body constructed here for each individual recipient
mm.Body = sb.ToString();
sb.Clear();
await mc.SendAsync(mm);
}
}
}
Now, if you want to do it concurrently, then you'll want to use Task.WhenAll:
using (var mc = new SmtpClient() {
Host = "127.0.0.1", // Current HMailServer installation - will be changed to external API
DeliveryMethod = SmtpDeliveryMethod.Network,
Port = 25,
UseDefaultCredentials = false,
Credentials = new NetworkCredential("Username", "Password")
})
{
var tasks = GetData().Select(async result =>
{
using(var mm = new MailMessage())
{
mm.To.Add(new MailAddress(result.Email, result.FirstName + " " + result.Surname));
mm.Subject = "Your monthly report";
mm.From = new MailAddress("noreply#example.com");
mm.ReplyToList.Add(new MailAddress("admin#example.com"));
var sb = new StringBuilder();
// Email body constructed here for each individual recipient
mm.Body = sb.ToString();
await mc.SendAsync(mm);
}
});
await Task.WhenAll(tasks);
}
(note that the StringBuilder is no longer shared)
I haven't used the SendGrid SMTP API at scale, but I have hit their REST API with a considerable number of requests.
I'm working on an application that can send emails out with attachments and it works, until I try special characters æ, ø, å.
I played a bit around testing different encodings and it looks like the subject is being encoded in ISO-8859-1 while the rest of the mail is encoded in UTF-8.
Here is my method that generates a Google Gmail API message
public Message CreateMessage(string to, string from, string body, string subject, GmailService service, string[] files = null, string bcc = null)
{
AE.Net.Mail.MailMessage message = new AE.Net.Mail.MailMessage()
{
Subject = subject,
Body = body,
From = new MailAddress(from),
};
message.To.Add(new MailAddress(to));
message.ReplyTo.Add(message.From);
message.Headers.Add("Content-Type", "text/plain; charset=utf-8");
if (bcc != null)
message.Bcc.Add(new MailAddress(bcc));
if (files != null)
{
foreach(string file in files)
{
using (var opennedFile = File.Open(file, FileMode.Open, FileAccess.Read))
using (MemoryStream stream = new MemoryStream())
{
string[] FileName = file.Split('\\');
opennedFile.CopyTo(stream);
message.Attachments.Add(new AE.Net.Mail.Attachment(stream.ToArray(), MediaTypeNames.Application.Octet, FileName[FileName.Length - 1], true));
}
}
}
var msgStr = new StringWriter();
message.Save(msgStr);
return new Message() {
Raw = Base64UrlEncode(msgStr.ToString()),
};
}
private static string Base64UrlEncode(string message)
{
var inputBytes = Encoding.GetEncoding("utf-8").GetBytes(message);
return Convert.ToBase64String(inputBytes).Replace('+', '-').Replace('/', '_').Replace("=", "");
}
message.ContentType = "text/plain; charset=utf-8" does not fix this issue and makes the attached files show in the body as Base64
You could use the following technique to use UTF-8 in the subject header.
=?charset?encoding?encoded-text?=
You could then use charset=utf-8, encoding=B (B = base64), and encoded subject as encoded-text.
Example
Subject: =?utf-8?B?aGVsbG8=?= // 'aGVsbG8=' is 'hello' in base64 format.
Remote Server returned '550 5.6.2 SMTPSEND.BareLinefeedsAreIllegal; message contains bare linefeeds, which cannot be sent via DATA'
var message = new MimeMessage();
message.From.Add(new MailboxAddress(nameFrom, mailboxFrom));
message.Subject = Subject;
message.To.Add(new MailboxAddress(mailboxTo));
var bodyBuilder = new BodyBuilder();
var multipart = new Multipart("mixed");
bodyBuilder.HtmlBody = "Test Body";
multipart.Add(bodyBuilder.ToMessageBody());
byte[] bytes = System.IO.File.ReadAllBytes("Test.gif");
var attachment = new MimePart("binary", "bin")
{
ContentObject = new ContentObject(new MemoryStream(bytes), ContentEncoding.Base64),
ContentDisposition = new ContentDisposition(ContentDisposition.Attachment),
ContentTransferEncoding = ContentEncoding.Binary,
FileName = "F2.pdf"
};
multipart.Add(attachment);
message.Body = multipart;
using (var client = new SmtpClient())
{
// For demo-purposes, accept all SSL certificates (in case the server supports STARTTLS)
client.ServerCertificateValidationCallback = (s, c, h, e) => true;
client.Connect(ssServer, ssPort, MailKit.Security.SecureSocketOptions.Auto);
// Note: since we don't have an OAuth2 token, disable
// the XOAUTH2 authentication mechanism.
client.AuthenticationMechanisms.Remove("XOAUTH2");
// Note: only needed if the SMTP server requires authentication
client.Authenticate(ssMailBox, ssMailBoxPassword);
client.Send(message);
}
I have already tried with encoding ContentEncoding.Base64 and ContentEncoding.Binary with same result. When I skip the attachment part the mail is sent correctly. The "Test.gif" is just a random gif I'm uploading.
I have read about CHUNKING or the BDAT command but not really sure about this or how to use it with mailkit ... any suggestions? I'm just trying to send a normal SMTP mail with attachments, this can't be that hard :|
The solution was, as suggested by #Jstedfast to change:
ContentTransferEncoding = ContentEncoding.Base64
I misunderstood and tried changing the ContentObject enconding.
Thanks #Jstedfast
Hi I am trying to send bulkmail with attachment using amazon ses. I can able to send mails with attachment but my to-mails are appearing for all the users that I have send. I am trying to add those destination mails in bcc fields but it is throwing an error Empty required header 'To'.
This is what I've already tried:
private static BodyBuilder GetMessageBody()
{
var body = new BodyBuilder()
{
HtmlBody = #"<p>Amazon SES Test body</p>",
TextBody = "Amazon SES Test body",
};
body.Attachments.Add(#"G:\me.jpg");
return body;
}
private static MimeMessage GetMessage()
{
var message = new MimeMessage();
List<string> to = new List<string>(50);
to.Add("xxxxxx#gmail.com");
to.Add("xxxxxx#gmail.com");
message.From.Add(new MailboxAddress(ConfigurationManager.AppSettings["senderaddress"], ConfigurationManager.AppSettings["senderaddress"]));
for (int i = 0; i < to.Count; i++)
{
message.Bcc.Add(new MailboxAddress(string.Empty,to[i]));
//message.To.Add(new MailboxAddress(string.Empty, "xxxxx#gmail.com"));
//message.To.Add(new MailboxAddress(string.Empty, "xxxxxx#gmail.com"));
}
message.Subject = "Amazon SES Test";
message.Body = GetMessageBody().ToMessageBody();
return message;
}
private static MemoryStream GetMessageStream()
{
var stream = new MemoryStream();
GetMessage().WriteTo(stream);
return stream;
}
private void SendEmails()
{
var credentals = new BasicAWSCredentials(ConfigurationManager.AppSettings["AccessKey"], ConfigurationManager.AppSettings["SecretAccessKey"]);
using (var client = new AmazonSimpleEmailServiceClient(credentals, RegionEndpoint.USEast1))
{
var sendRequest = new SendRawEmailRequest { RawMessage = new RawMessage(GetMessageStream()) };
try
{
var response = client.SendRawEmail(sendRequest);
}
catch (Exception e)
{
}
}
}
You need at least 1 email address in to 'To' field. Perhaps send the email to yourself and add the others as a BCC.
Instead of using the BCC header to send out bulk mails, IMHO you should send one email with a clear "to" header to each of the recipients. So instead looping through recipients and adding them to BCC you should rather create and send a message there.