Search code examples
c#amazon-web-servicesamazon-sesmailmessage

Double period issue with Amazon SES, attachments, dot-stuffing when using C# SDK


As anyone who has worked with it knows, to send an email through the Amazon SES C# SDK with an attachment, you must use the SendRawEmail function (which is extremely stinky). In order to do so, you either hand-code a MIME message or rather convert a System.Net.Mail.MailMessage object to a MemoryStream. This has all been working fine, but I've encountered an issue. After much digging, I've been able to find the problem and replicate it, and it seems to be a by-product of SMTP dot-stuffing.

The issue is that in certain scenarios, when the MailMessage is converted to a raw MIME message, if a period in the message body is wrapped to the beginning of a line in the raw message then it is dot-stuffed (properly I assume). However, it doesn't seem to be handled either inside the SDK or on the SES side of things because the email comes through with double periods. This can be replicated via the following example console app code...

static void Main(string[] args)
{
    var to = new List<string> { _toAddress };
    var subject = "TEST MESSAGE";
    var message = $"This is a carefully crafted HTML email message body such that you should see a single period right here ->. However, you'll see <strong>two periods</strong> in the email instead of one period like originally given in the code.";

    var body = $"<br />Hello,<br /><br />{message}<br /><br />Sincerely,<br /><br />Your Tester";

    var result = SendEmail(to, subject, body, null, isHtml: true);
    Console.WriteLine(result ? "Successfully sent message" : "Failed to send message");

    if (Debugger.IsAttached)
    {
        Console.WriteLine("Press any key to continue...");
        Console.ReadLine();
    }
}

private static bool SendEmail(List<string> to, string subject, string body, List<string> attachmentFilePaths = null, bool isHtml = false)
{
    var message = new MailMessage
    {
        From = new MailAddress(_fromAddress),
        Subject = subject,
        Body = body,
        IsBodyHtml = isHtml
    };

    foreach (var address in to)
    {
        message.To.Add(address.Trim());
    }

    if (attachmentFilePaths?.Any() == true)
    {
        foreach (var filePath in attachmentFilePaths)
        {
            message.Attachments.Add(new Attachment(filePath));
        }
    }

    try
    {
        var creds = new BasicAWSCredentials(_SESAccessKey, _SESSecretKey);
        using (var client = new AmazonSimpleEmailServiceClient(creds, RegionEndpoint.USEast1))
        {
            var request = new SendRawEmailRequest { RawMessage = new RawMessage { Data = ConvertMailMessageToMemoryStream(message) } };
            Console.WriteLine($"RawMessage.Data...\r\n\r\n{Encoding.ASCII.GetString(request.RawMessage.Data.ToArray())}");

            client.SendRawEmail(request);
            return true;
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine($"AmazonSESHelper.SendEmail => Exception: {ex.Message}");
    }

    return false;
}

// Have to do this reflection crap in order to send attachments to the SES API
// From http://stackoverflow.com/questions/29532152/create-mime-mail-with-attachment-for-aws-ses-c-sharp/29533336#29533336
private static MemoryStream ConvertMailMessageToMemoryStream(MailMessage message)
{
    var stream = new MemoryStream();
    var assembly = typeof(SmtpClient).Assembly;
    var mailWriterType = assembly.GetType("System.Net.Mail.MailWriter");

    var mailWriterConstructor = mailWriterType.GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, null, new[] { typeof(Stream) }, null);
    var mailWriter = mailWriterConstructor.Invoke(new object[] { stream });

    var sendMethod = typeof(MailMessage).GetMethod("Send", BindingFlags.Instance | BindingFlags.NonPublic);
    sendMethod.Invoke(message, BindingFlags.Instance | BindingFlags.NonPublic, null, new[] { mailWriter, true, true }, null);

    var closeMethod = mailWriter.GetType().GetMethod("Close", BindingFlags.Instance | BindingFlags.NonPublic);
    closeMethod.Invoke(mailWriter, BindingFlags.Instance | BindingFlags.NonPublic, null, new object[] { }, null);

    return stream;
}

The raw MIME message that gets outputted is...

X-Sender: [email protected]
X-Receiver: [email protected]
MIME-Version: 1.0
From: [email protected]
To: [email protected]
Date: 2 Dec 2016 11:45:36 -0500
Subject: TEST MESSAGE
Content-Type: text/html; charset=us-ascii
Content-Transfer-Encoding: quoted-printable

<br />Hello,<br /><br />This is a carefully crafted HTML email me=
ssage body such that you should see a single period right here ->=
.. However, you'll see <strong>two periods</strong> in the email i=
nstead of one period like originally given in the code.<br /><br =
/>Sincerely,<br /><br />Your Tester

You can see the dot-stuffing (again, assuming that's proper per SMTP), but it doesn't get un-done after the send as can be seen when I receive the email...

enter image description here

I can get this to work if I add in @jstedfast MimeKit (which is great) but adds another dependency in all our apps, which is not my favorite thing to do. Before anything, I wanted to throw it out here to see if there's anything I'm missing. If not, it would be nice to see SES acknowledge this as an issue and fix it. Otherwise, I have to decide between another library dependency or jumping ship off SES to another mail provider.


Solution

  • It turns out this is an issue on the AWS side, but they will not be addressing any time soon (or even at all). I posted an issue to the GitHub repo as can be seen here:

    https://github.com/aws/aws-sdk-net/issues/501

    I have since added in MimeKit to our apps to replace the conversion to mime via reflection. The new ConvertMailMessageToMemoryStream is:

    private static MemoryStream ConvertMailMessageToMemoryStream(MailMessage message)
    {
        var stream = new MemoryStream();
        var mimeMessage = MimeMessage.CreateFromMailMessage(message);
        mimeMessage.Prepare(EncodingConstraint.None);
        mimeMessage.WriteTo(stream);
        return stream;
    }