Now implemented changes suggested in my previous post and now am back again for the internet to tear me and my shitty code down.

PatchNotes Extracted templateRetriever class from the email service thus making it a little smaller. Put the email bodyTEXT in embedded HTML file so the program is more future proof. Change method name of ReadFully to StreamToByteArray to be clearer Removed duplicate code from AddRecipients

Did not remove return type to void as this would make it harder to use the method for others, and think it is more readable to return it. Please tell me if this is an anti-pattern/bad practice

Old post: Email Service - Sending an email with azure blob storage

       /// <summary> Handles emails using Office365 </summary>
        public class EmailService : IEmailService
            private readonly IBlobStorageService _blobStorageService;
            private readonly EmailSettings _emailSettings;

            public EmailService(IBlobStorageService blobStorageService, EmailSettings emailSettings)
                _emailSettings = emailSettings ?? throw new ArgumentNullException("Email configuration cannot be null.");

                _blobStorageService = blobStorageService;

            /// <summary> Sends an email using Office365 with the credentials specified in configuration. </summary>
            /// <param name="motionActivity"></param>
            /// <param name="mediaResults"></param>
            /// <returns> true or false </returns>
            /// <exception cref="ArgumentException"></exception>
            public async Task<bool> SendEmail(MotionActivity motionActivity, List<MediaAnalysis> mediaResults)
                if (motionActivity == null)
                    throw new ArgumentException("Motion activity cannot be null");
                if (mediaResults == null)
                    throw new ArgumentException("Media result should not be null");

                // Filter out all results below 60% probability
                var mediaResultsAbove60Percent = mediaResults.FindAll(x => x.Probability >= 60);

                // If no pictures remain afterwards, return
                if (mediaResultsAbove60Percent.Count == 0) return false;

                var highestProbability = mediaResultsAbove60Percent.Max(x => x.Probability);

                var message = await CreateEmailMessage(motionActivity, highestProbability, mediaResultsAbove60Percent);

                var fullMessage = AddRecipients(message, highestProbability);

                return await SendMessage(fullMessage).ConfigureAwait(false);

            /// <summary> Sends the message using MimeMessage/office365 </summary>
            /// <param name="message"> The message we are sending </param>
            /// <returns>Whether or not the message send was successful.</returns>
            private async Task<bool> SendMessage(MimeMessage message)
                    using (var client = new SmtpClient())
                        await client.ConnectAsync(_emailSettings.Host, _emailSettings.Port, false);
                        await client.AuthenticateAsync(_emailSettings.FromAddress, _emailSettings.Password);

                        await client.SendAsync(message);
                        await client.DisconnectAsync(true);
                catch (Exception ex)
                        "An error occurred trying to send an Office 365 email: {message}, {innerException}, {stacktrace}",
                        ex.Message, ex.InnerException, ex.StackTrace);
                    return false;

                return true;

            /// <summary> Creates the templates used for emails </summary>
            /// <param name="motionActivity"> is the data of the entire time we collected motion for ONE camera </param>
            /// <param name="highestProbability"> the highest probability we found that we think there is a canister </param>
            /// <param name="mediaResultsAbove60Percent"> images above 60% </param>
            private async Task<MimeMessage> CreateEmailMessage(MotionActivity motionActivity, double highestProbability,
                IEnumerable<MediaAnalysis> mediaResultsAbove60Percent)
                var (subject, body) = await TemplateRetriever.CreateEmailTemplate(motionActivity, highestProbability);

                var builder = new BodyBuilder {HtmlBody = body};
                var message = new MimeMessage();
                message.From.Add(new MailboxAddress(_emailSettings.DisplayName, _emailSettings.FromAddress));
                message.Subject = subject;

                var emailWithAttachments = await AddImageAttachment(mediaResultsAbove60Percent, message, builder);

                emailWithAttachments.Body = builder.ToMessageBody();

                return emailWithAttachments;

            /// <summary> Adds all recipients to our email </summary>
            /// <param name="email"> the email we are sending </param>
            /// <param name="highestProbability"> the highest probability from all media results </param>
            private MimeMessage AddRecipients(MimeMessage email, double highestProbability)
                var recipients = highestProbability >= 75 ? _emailSettings.FullRecipients : _emailSettings.Recipients;

                foreach (var recipient in recipients)
                    email.To.Add(new MailboxAddress(recipient));

                return email;

            /// <summary> Takes all images above 60% probability and adds the image to the email</summary>
            /// <param name="mediaResultsAbove60Percent"> images above 60% </param>
            /// <param name="email"> the email we will be sending </param>
            /// <param name="bodyBuilder"> the body builder for emails </param>
            /// <returns> the email </returns>
            private async Task<MimeMessage> AddImageAttachment(IEnumerable<MediaAnalysis> mediaResultsAbove60Percent,
                MimeMessage email, BodyBuilder bodyBuilder)
                foreach (var mediaResult in mediaResultsAbove60Percent)
                    if (mediaResult.MediaUrl == null) continue;

                    var download = await _blobStorageService.DownloadFile(mediaResult.MediaUrl).ConfigureAwait(false);
                    var byteArray = StreamToByteArray(download.Content);

                    bodyBuilder.Attachments.Add(mediaResult.MediaUrl, byteArray);

                return email;

            /// <summary> Helper method for add image attachment. Converts stream to byte array. </summary>
            /// <param name="input"> the image we are converting </param>
            /// <returns> the byte array </returns>
            private static byte[] StreamToByteArray(Stream input)
                using (var ms = new MemoryStream())
                    return ms.ToArray();

Did split the class into two

public static class TemplateRetriever
        /// <summary> Creates the templates used for emails </summary>
        /// <param name="motionActivity"> is the data of the entire time we collected motion for ONE camera </param>
        /// <param name="highestProbability"> the highest probability we found that we think there is a canister </param>
        public static async Task<Tuple<string, string>> CreateEmailTemplate(MotionActivity motionActivity,
            double highestProbability)
            var dataHash = new Dictionary<string, object>
                {"highestProbability", highestProbability},
                {"motionActivity", motionActivity}

            var subject = string.Empty;
            var bodyTemplate = string.Empty;

            //Changes subject headline based on probability
            if (highestProbability >= 60 && highestProbability <= 74)
                subject = $"Chameleon Detect: Audit Image - {highestProbability}%";
                bodyTemplate = await GetTemplate("HighProbabilityTemplate.html", dataHash);

            if (highestProbability >= 75 && highestProbability <= 87)
                subject = $"Chameleon Detect: Warning Image - {highestProbability}%";
                bodyTemplate = await GetTemplate("MediumProbabilityTemplate.html", dataHash);

            if (highestProbability >= 88 && highestProbability <= 100)
                subject = $"Chameleon Detect: High Risk Image - {highestProbability}%";
                bodyTemplate = await GetTemplate("LowProbabilityTemplate.html", dataHash);

            return new Tuple<string, string>(subject, bodyTemplate);

        private static async Task<string> GetTemplate(string fileName, Dictionary<string, object> dataHash)
            // Determine path
            var assembly = Assembly.GetExecutingAssembly();

            var resourcePath = assembly.GetManifestResourceNames()
                .Single(str => str.EndsWith(fileName));

            var stubble = new StubbleBuilder().Build();

            using (var stream = assembly.GetManifestResourceStream(resourcePath))
            using (var streamReader = new StreamReader(stream ?? throw new Exception(), Encoding.UTF8))
                var content = await streamReader.ReadToEndAsync();
                var output = await stubble.RenderAsync(content, dataHash);
                return output;

And the HTML file being read using the Stubble HTML template framework https://github.com/StubbleOrg/Stubble/blob/master/docs/how-to.md


<p>Dear Concierge </p>

<p>It is possible that an explosive canister has entered the building at the address above. Please review the attached photographs and take precautionary action.</p>

<p>Motion Activity: {{motionActivity}}</p>


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.