<|start_filename|>src/Serilog.Sinks.Email/Sinks/Email/EmailSink.cs<|end_filename|>
// Copyright 2014 Serilog Contributors
// 
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// 
//     http://www.apache.org/licenses/LICENSE-2.0
// 
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Serilog.Events;
using Serilog.Formatting;
using Serilog.Sinks.PeriodicBatching;
using System.Linq;

namespace Serilog.Sinks.Email
{
    class EmailSink : IBatchedLogEventSink, IDisposable
    {
        private static readonly char[] MailAddressesSplitCharacters = { ';', ',' };
        readonly EmailConnectionInfo _connectionInfo;
        readonly IEmailTransport _emailTransport;

        readonly ITextFormatter _textFormatter;

        readonly ITextFormatter _subjectLineFormatter;

        /// <summary>
        /// Construct a sink emailing with the specified details.
        /// </summary>
        /// <param name="connectionInfo">Connection information used to construct the SMTP client and mail messages.</param>
        /// <param name="textFormatter">Supplies culture-specific formatting information, or null.</param>
        /// <param name="subjectLineFormatter">Supplies culture-specific formatting information, or null.</param>
        /// <exception cref="System.ArgumentNullException">connectionInfo</exception>
        public EmailSink(EmailConnectionInfo connectionInfo, ITextFormatter textFormatter, ITextFormatter subjectLineFormatter)
        {
            if (connectionInfo == null) throw new ArgumentNullException(nameof(connectionInfo));

            _connectionInfo = connectionInfo;
            _emailTransport = _connectionInfo.CreateEmailTransport();
            _textFormatter = textFormatter;
            _subjectLineFormatter = subjectLineFormatter;
        }
        
        /// <summary>
        /// Emit a batch of log events, running asynchronously.
        /// </summary>
        /// <param name="events">The events to emit.</param>
        /// <remarks>Override either <see cref="PeriodicBatchingSink.EmitBatch"/> or <see cref="PeriodicBatchingSink.EmitBatchAsync"/>,
        /// not both.</remarks>
        public async Task EmitBatchAsync(IEnumerable<LogEvent> events)
        {
            if (events == null)
                throw new ArgumentNullException(nameof(events));

            var payload = new StringWriter();

            foreach (var logEvent in events)
            {
                _textFormatter.Format(logEvent, payload);
            }

            var subject = new StringWriter();
            _subjectLineFormatter.Format(events.OrderByDescending(e => e.Level).First(), subject);

            var email = new EmailMessage(
                _connectionInfo.FromEmail,
                _connectionInfo.ToEmail.Split(MailAddressesSplitCharacters, StringSplitOptions.RemoveEmptyEntries),
                subject.ToString(),
                payload.ToString(),
                _connectionInfo.IsBodyHtml);

            await _emailTransport.SendMailAsync(email);
        }

        public Task OnEmptyBatchAsync()
        {
            return Task.FromResult(false);
        }

        public void Dispose()
        {
            _emailTransport.Dispose();
        }
    }
}


<|start_filename|>test/Serilog.Sinks.Email.Tests/EmailSinkTests.cs<|end_filename|>
﻿using Moq;
using Serilog.Debugging;
using Serilog.Events;
using Serilog.Formatting.Display;
using Serilog.Parsing;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Xunit;

namespace Serilog.Sinks.Email.Tests
{
    public class EmailSinkTests
    {
        [Fact(Skip = "Requires a localhost mail server")]
        public void Works()
        {
            var selfLogMessages = new List<string>();
            SelfLog.Enable(selfLogMessages.Add);

            using (var emailLogger = new LoggerConfiguration()
                .WriteTo.Email(
                    fromEmail: "<EMAIL>",
                    toEmail: "<EMAIL>",
                    mailServer: "localhost",
                    outputTemplate: "[{Level}] {Message}{NewLine}{Exception}",
                    mailSubject: "subject")
                .CreateLogger())
            {
                emailLogger.Information("test {test}", "test");
            }

            Assert.Equal(Enumerable.Empty<string>(), selfLogMessages);
        }

        [Fact(Skip = "Requires a smtp mail server")]
        public void WorksMulitpleEventsInOneMail()
        {
            var selfLogMessages = new List<string>();
            SelfLog.Enable(selfLogMessages.Add);

            using (var emailLogger = new LoggerConfiguration()
                .WriteTo.Email(
                    fromEmail: "<EMAIL>",
                    toEmail: "<EMAIL>",
                    mailServer: "smtpserver.local",
                    outputTemplate: "[{Level}] {Message}{NewLine}{Exception}",
                    mailSubject: "test subject")
                .CreateLogger())
            {
                emailLogger.Information("first test {test}", "test1");
                emailLogger.Error("second {test}", "test2");
                emailLogger.Fatal("third {test}", "test3");
            }

            Assert.Equal(Enumerable.Empty<string>(), selfLogMessages);
        }

        [Fact]
        public void EmailTransportIsCreatedWhenEmailSinkIsConstructed()
        {
            var (emailConnectionInfoMock, emailTransportMock) = CreateEmailTransportConnectionInfoMocks();
            CreateDefaultEmailSink(emailConnectionInfoMock.Object);

            emailConnectionInfoMock.Verify(eci => eci.CreateEmailTransport(), Times.Once);
            emailTransportMock.Verify(eci => eci.Dispose(), Times.Never);
        }

        [Fact]
        public void EmailTransportIsDisposedWhenEmailSinkIsDisposed()
        {
            var (emailConnectionInfoMock, emailTransportMock) = CreateEmailTransportConnectionInfoMocks();
            var emailSink = CreateDefaultEmailSink(emailConnectionInfoMock.Object);

            emailSink.Dispose();

            emailTransportMock.Verify(eci => eci.Dispose(), Times.Once);
        }

        [Fact]
        [UseCulture("en-us")]
        public async Task SendEmailIsCorrectlyCalledWhenEventAreLogged()
        {
            EmailMessage actual = null;
            var (emailConnectionInfoMock, emailTransportMock) =
                CreateEmailTransportConnectionInfoMocks(email => actual = email);
            var emailConnectionInfo = emailConnectionInfoMock.Object;
            emailConnectionInfo.ToEmail = "<EMAIL>";
            emailConnectionInfo.FromEmail = "<EMAIL>";
            emailConnectionInfo.IsBodyHtml = true;
            var emailSink = CreateDefaultEmailSink(emailConnectionInfoMock.Object);

            await emailSink.EmitBatchAsync(new[] {
                new LogEvent(
                    DateTimeOffset.Now,
                    LogEventLevel.Error,
                    new ArgumentOutOfRangeException("parameter1", "Message of the exception"),
                    new MessageTemplate(@"Subject",
                        new MessageTemplateToken[]
                        {
                            new PropertyToken("Message", "A multiline" + Environment.NewLine
                                                                       + "Message")
                        })
                    , Enumerable.Empty<LogEventProperty>())});
            emailSink.Dispose();

            emailTransportMock.Verify(et => et.SendMailAsync(It.IsAny<EmailMessage>()), Times.Once);

            Assert.Equal("[Error] A multiline" + Environment.NewLine
                        + "Message" + Environment.NewLine
                        + "System.ArgumentOutOfRangeException: Message of the exception"
#if NEW_ARGUMENTOUTOFRANGEEXCEPTION_MESSAGE
                        + " (Parameter 'parameter1')"
#else
                        + Environment.NewLine + "Parameter name: parameter1"
#endif
                        + Environment.NewLine + "", actual.Body);
            Assert.Equal(@"[Error] A multiline" + Environment.NewLine
                        + "Message" + Environment.NewLine
                        + "System.ArgumentOutOfRangeException: Message of the exception"
#if NEW_ARGUMENTOUTOFRANGEEXCEPTION_MESSAGE
                        + " (Parameter 'parameter1')"
#else
                        + Environment.NewLine + "Parameter name: parameter1"
#endif
                        + Environment.NewLine + "", actual.Subject);
            Assert.Equal("<EMAIL>", actual.From);
            Assert.Equal(new[] { "<EMAIL>" }, actual.To);
            Assert.True(actual.IsBodyHtml);
        }

        private EmailSink CreateDefaultEmailSink(EmailConnectionInfo emailConnectionInfo)
        {
            var formatter = new MessageTemplateTextFormatter("[{Level}] {Message}{NewLine}{Exception}", null);
            var subjectLineFormatter = new MessageTemplateTextFormatter("[{Level}] {Message}{NewLine}{Exception}", null);

            var emailSink = new EmailSink(
                emailConnectionInfo,
                formatter,
                subjectLineFormatter);
            return emailSink;
        }

        private (Mock<EmailConnectionInfo> EmailConnectionInfoMock, Mock<IEmailTransport> EmailTransportMock)
            CreateEmailTransportConnectionInfoMocks()
        {
            return CreateEmailTransportConnectionInfoMocks(_ => { });
        }

        private (Mock<EmailConnectionInfo> EmailConnectionInfoMock, Mock<IEmailTransport> EmailTransportMock)
            CreateEmailTransportConnectionInfoMocks(Action<EmailMessage> emailSend)
        {
            var emailTransportMock = new Mock<IEmailTransport>();
            emailTransportMock.Setup(et => et.SendMailAsync(It.IsAny<EmailMessage>()))
                .Callback<EmailMessage>(email => emailSend(email))
                .Returns(Task.Factory.StartNew(() => { }));
            var emailTransport = emailTransportMock.Object;
            var emailConnectionInfoMock = new Mock<EmailConnectionInfo>();
            emailConnectionInfoMock
                .Setup(eci => eci.CreateEmailTransport())
                .Returns(emailTransport);
            return (emailConnectionInfoMock, emailTransportMock);
        }
    }
}


<|start_filename|>src/Serilog.Sinks.Email/Sinks/Email/EmailMessage.cs<|end_filename|>
﻿using System.Collections.Generic;

namespace Serilog.Sinks.Email
{
    internal class EmailMessage
    {
        public EmailMessage(string from, IEnumerable<string> to, string subject, string body, bool isBodyHtml)
        {
            From = from;
            To = to;
            Subject = subject;
            Body = body;
            IsBodyHtml = isBodyHtml;
        }
        public string From { get; }

        public string Subject { get; }

        public string Body { get; }

        public bool IsBodyHtml { get; }

        public IEnumerable<string> To { get; }
    }
}


<|start_filename|>src/Serilog.Sinks.Email/Sinks/Email/SystemMailEmailTransport.cs<|end_filename|>
﻿// Copyright 2014 Serilog Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#if SYSTEM_NET
using System.ComponentModel;
using System.Net.Mail;
using System.Text;
using System.Threading.Tasks;
using Serilog.Debugging;

namespace Serilog.Sinks.Email
{
    class SystemMailEmailTransport : IEmailTransport
    {
        readonly SmtpClient _smtpClient;

        public SystemMailEmailTransport(EmailConnectionInfo connectionInfo)
        {
            _smtpClient = CreateSmtpClient(connectionInfo);
            _smtpClient.SendCompleted += SendCompletedCallback;
        }

        public async Task SendMailAsync(EmailMessage emailMessage)
        {
            var mailMessage = new MailMessage
            {
                From = new MailAddress(emailMessage.From),
                Subject = emailMessage.Subject,
                Body = emailMessage.Body,
                BodyEncoding = Encoding.UTF8,
                SubjectEncoding = Encoding.UTF8,
                IsBodyHtml = emailMessage.IsBodyHtml
            };

            foreach (var recipient in emailMessage.To)
            {
                mailMessage.To.Add(recipient);
            }

            await _smtpClient.SendMailAsync(mailMessage);
        }

        public void Dispose()
        {
            _smtpClient.Dispose();
        }

        SmtpClient CreateSmtpClient(EmailConnectionInfo connectionInfo)
        {
            var smtpClient = new SmtpClient();
            if (!string.IsNullOrWhiteSpace(connectionInfo.MailServer))
            {
                if (connectionInfo.NetworkCredentials == null)
                {
                    smtpClient.UseDefaultCredentials = true;
                }
                else
                {
                    smtpClient.Credentials = connectionInfo.NetworkCredentials;
                }

                smtpClient.Host = connectionInfo.MailServer;
                smtpClient.Port = connectionInfo.Port;
                smtpClient.EnableSsl = connectionInfo.EnableSsl;
            }

            return smtpClient;
        }

        /// <summary>
        ///     Reports if there is an error in sending an email
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        static void SendCompletedCallback(object sender, AsyncCompletedEventArgs e)
        {
            if (e.Cancelled)
            {
                SelfLog.WriteLine("Received failed result {0}: {1}", "Cancelled", e.Error);
            }

            if (e.Error != null)
            {
                SelfLog.WriteLine("Received failed result {0}: {1}", "Error", e.Error);
            }
        }
    }
}
#endif
