Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 101 additions & 32 deletions Rnwood.Smtp4dev.Tests/Controllers/MessagesControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public class MessagesControllerTests
public async Task GetMessage_ValidMime()
{
DateTime startDate = DateTime.Now;
DbModel.Message testMessage1 = await GetTestMessage1();
var testMessage1 = await GetTestMessage1();

TestMessagesRepository messagesRepository = new TestMessagesRepository(testMessage1);
MessagesController messagesController = new MessagesController(messagesRepository, null);
Expand All @@ -38,9 +38,9 @@ public async Task GetMessage_ValidMime()
Assert.Equal(testMessage1.Id, result.Id);
Assert.InRange(result.ReceivedDate, startDate, DateTime.Now);
Assert.Equal("from@message.com", result.From);
Assert.Equal(new[]{"to@message.com"}, result.To);
Assert.Equal(new[]{"to@envelope.com"}, result.Bcc);
Assert.Equal(new[]{"cc@message.com"}, result.Cc);
Assert.Equal(new[] { "to@message.com" }, result.To);
Assert.Equal(new[] { "to@envelope.com" }, result.Bcc);
Assert.Equal(new[] { "cc@message.com" }, result.Cc);
Assert.Equal("subject", result.Subject);

var allParts = result.Parts.Flatten(p => p.ChildParts).ToList();
Expand All @@ -65,7 +65,7 @@ public async Task GetMessage_ValidMime()
private static readonly string message1HtmlBody = "<html>Hi</html>";
private static readonly string message1TextBody = "Hi";

private static async Task<DbModel.Message> GetTestMessage1(bool includeHtmlBody=true, bool includeTextBody=true)
private static async Task<DbModel.Message> GetTestMessage1(bool includeHtmlBody = true, bool includeTextBody = true)
{
MimeMessage mimeMessage = new MimeMessage();
mimeMessage.From.Add(InternetAddress.Parse("from@message.com"));
Expand Down Expand Up @@ -130,7 +130,37 @@ public async Task GetMessage_ValidMime()

var dbMessage = await new MessageConverter().ConvertAsync(message, [to]);
dbMessage.Mailbox = new DbModel.Mailbox { Name = MailboxOptions.DEFAULTNAME };


return dbMessage;
}

private static async Task<DbModel.Message> GetTestMessageWithContent(string subject, string content, string from = "from@from.com", string to = "to@to.com")
{
MimeMessage mimeMessage = new MimeMessage();
mimeMessage.From.Add(InternetAddress.Parse(from));
mimeMessage.To.Add(InternetAddress.Parse(to));

mimeMessage.Subject = subject;
BodyBuilder bodyBuilder = new BodyBuilder();
bodyBuilder.HtmlBody = "<html>" + content + "</html>";
bodyBuilder.TextBody = content;

mimeMessage.Body = bodyBuilder.ToMessageBody();

MemoryMessageBuilder memoryMessageBuilder = new MemoryMessageBuilder();
memoryMessageBuilder.Recipients.Add(to);
memoryMessageBuilder.From = from;
memoryMessageBuilder.ReceivedDate = DateTime.Now;
using (var messageData = await memoryMessageBuilder.WriteData())
{
mimeMessage.WriteTo(messageData);
}

IMessage message = await memoryMessageBuilder.ToMessage();

var dbMessage = await new MessageConverter().ConvertAsync(message, [to]);
dbMessage.Mailbox = new DbModel.Mailbox { Name = MailboxOptions.DEFAULTNAME };

return dbMessage;
}

Expand All @@ -142,7 +172,7 @@ public async Task GetMessage_ValidMime()
mimeMessage.Cc.Add(InternetAddress.Parse("cc@message.com"));

mimeMessage.Subject = "subject";
MimePart body = new MimePart( new ContentType("text", "html"));
MimePart body = new MimePart(new ContentType("text", "html"));
body.ContentTransferEncoding = ContentEncoding.QuotedPrintable;
body.ContentType.CharsetEncoding = encoding;
body.Content = new MimeContent(new MemoryStream(encoding.GetBytes(QPMESSAGE_BODY)));
Expand All @@ -166,9 +196,9 @@ public async Task GetMessage_ValidMime()
[Fact]
public async Task GetSummaries_NoSearch_AllMessagesReturned()
{
DbModel.Message testMessage1 = await GetTestMessage("Message subject1");
DbModel.Message testMessage2 = await GetTestMessage("Message subject2");
DbModel.Message testMessage3 = await GetTestMessage("Message subject3");
var testMessage1 = await GetTestMessage("Message subject1");
var testMessage2 = await GetTestMessage("Message subject2");
var testMessage3 = await GetTestMessage("Message subject3");
TestMessagesRepository messagesRepository = new TestMessagesRepository(testMessage1, testMessage2, testMessage3);
MessagesController messagesController = new MessagesController(messagesRepository, null);

Expand All @@ -179,9 +209,9 @@ public async Task GetSummaries_NoSearch_AllMessagesReturned()
[Fact]
public async Task GetSummaries_Search_MatchingMessagesReturned()
{
DbModel.Message testMessage1 = await GetTestMessage("Message subject1");
DbModel.Message testMessage2 = await GetTestMessage("Message subject2");
DbModel.Message testMessage3 = await GetTestMessage("Message subject3");
var testMessage1 = await GetTestMessage(subject: "Message subject1");
var testMessage2 = await GetTestMessage("Message subject2");
var testMessage3 = await GetTestMessage("Message subject3");
var sqlLiteForTesting = new SqliteInMemory();
var context = new Smtp4devDbContext(sqlLiteForTesting.ContextOptions);
MessagesRepository messagesRepository =
Expand All @@ -194,57 +224,96 @@ public async Task GetSummaries_Search_MatchingMessagesReturned()
result.Results.Select(m => m.Id).Should().BeEquivalentTo(new[] { testMessage2.Id });
}

[Theory]
[InlineData("subject1", "content1", 1)]
[InlineData("subject2", "content2", 1)]
[InlineData("subject3", "content3", 1)]
[InlineData("subject3", "", 2)]
[InlineData("subject3", "content4", 1)]
public async Task GetSummaries_Find_MatchingMessagesReturned(string subjectToSearch, string contentSearchPattern, int expectedResultCount)
{
// Arrange
var testMessage1 = await GetTestMessageWithContent("Message subject1", "This is my test content1");
var testMessage2 = await GetTestMessageWithContent("Message subject2", "This is my test content2");
var testMessage3 = await GetTestMessageWithContent("Message subject3", "This is my test content3");
var testMessage4 = await GetTestMessageWithContent("Message subject3", "This is my test content4");
testMessage4.Id = Guid.NewGuid();
var sqlLiteForTesting = new SqliteInMemory();
var context = new Smtp4devDbContext(sqlLiteForTesting.ContextOptions);
MessagesRepository messagesRepository = new MessagesRepository(Substitute.For<ITaskQueue>(), Substitute.For<NotificationsHub>(), context);
messagesRepository.DbContext.Messages.AddRange(testMessage1, testMessage2, testMessage3, testMessage4);
await messagesRepository.DbContext.SaveChangesAsync();

// Act
MessagesController messagesController = new MessagesController(messagesRepository, null);
var result = messagesController.Find(new ApiModel.SearchMessagesCriteria("", subjectToSearch, contentSearchPattern, null));

// Assert
result.Results.Count.Should().Be(expectedResultCount);
if (expectedResultCount > 1)
{
result.Results[0].Subject.Should().Contain(subjectToSearch);
result.Results[1].Subject.Should().Contain(subjectToSearch);
}
else
{
var mailContent = await messagesController.GetMessageSource(result.Results[0].Id);
result.Results.First().Subject.Should().Contain(subjectToSearch);
mailContent.Should().Contain(contentSearchPattern);
}
}

[Fact]
public async Task GetHtmlBody()
{
DbModel.Message testMessage1 = await GetTestMessage1();
var testMessage1 = await GetTestMessage1();
TestMessagesRepository messagesRepository = new TestMessagesRepository(testMessage1);
MessagesController messagesController = new MessagesController(messagesRepository, null);

var result = await messagesController.GetMessageHtml(testMessage1.Id);
Assert.Equal(message1HtmlBody, result.Value);
}

[Fact]
public async Task GetTextBody()
{
DbModel.Message testMessage1 = await GetTestMessage1();
var testMessage1 = await GetTestMessage1();
TestMessagesRepository messagesRepository = new TestMessagesRepository(testMessage1);
MessagesController messagesController = new MessagesController(messagesRepository, null);

string text = (await messagesController.GetMessagePlainText(testMessage1.Id)).Value;
Assert.Equal(message1TextBody, text);
}

[Fact]
public async Task GetHtmlBody_WhenThereIsntOne_ReturnsNotFound()
{
DbModel.Message testMessage1 = await GetTestMessage1(includeHtmlBody:false);
var testMessage1 = await GetTestMessage1(includeHtmlBody: false);
TestMessagesRepository messagesRepository = new TestMessagesRepository(testMessage1);
MessagesController messagesController = new MessagesController(messagesRepository, null);

var result = await messagesController.GetMessageHtml(testMessage1.Id);
Assert.IsType<NotFoundObjectResult>(result.Result);
}

[Fact]
public async Task GetTextBody_WhenThereIsntOne_ReturnsNotFound()
{
DbModel.Message testMessage1 = await GetTestMessage1(includeTextBody:false);
var testMessage1 = await GetTestMessage1(includeTextBody: false);
TestMessagesRepository messagesRepository = new TestMessagesRepository(testMessage1);
MessagesController messagesController = new MessagesController(messagesRepository, null);

var result= await messagesController.GetMessagePlainText(testMessage1.Id);
var result = await messagesController.GetMessagePlainText(testMessage1.Id);
Assert.IsType<NotFoundObjectResult>(result.Result);
}
[Fact]
public async Task GetNewSummaries_NoBookmark_AllMessagesReturned()
{
DbModel.Message testMessage1 = await GetTestMessage1();
DbModel.Message testMessage2 = await GetTestMessage1();
DbModel.Message testMessage3 = await GetTestMessage1();
var testMessage1 = await GetTestMessage1();
var testMessage2 = await GetTestMessage1();
var testMessage3 = await GetTestMessage1();
TestMessagesRepository messagesRepository = new TestMessagesRepository(testMessage1, testMessage2, testMessage3);

MessagesController messagesController = new MessagesController(messagesRepository, null);

var result = messagesController.GetNewSummaries(null);
Expand All @@ -254,9 +323,9 @@ public async Task GetNewSummaries_NoBookmark_AllMessagesReturned()
[Fact]
public async Task GetNewSummaries_NoBookmark_NewerMessagesReturned()
{
DbModel.Message testMessage1 = await GetTestMessage1();
DbModel.Message testMessage2 = await GetTestMessage1();
DbModel.Message testMessage3 = await GetTestMessage1();
var testMessage1 = await GetTestMessage1();
var testMessage2 = await GetTestMessage1();
var testMessage3 = await GetTestMessage1();
TestMessagesRepository messagesRepository = new TestMessagesRepository(testMessage1, testMessage2, testMessage3);
MessagesController messagesController = new MessagesController(messagesRepository, null);

Expand All @@ -267,7 +336,7 @@ public async Task GetNewSummaries_NoBookmark_NewerMessagesReturned()
[Fact]
public async Task GetPartContent()
{
DbModel.Message testMessage1 = await GetTestMessage1();
var testMessage1 = await GetTestMessage1();
TestMessagesRepository messagesRepository = new TestMessagesRepository(testMessage1);
MessagesController messagesController = new MessagesController(messagesRepository, null);

Expand All @@ -286,7 +355,7 @@ public async Task GetPartContent()
[InlineData("iso-8859-1")]
public async Task GetMessageSource_QPMessage_ReturnsNotHeadersDecodedContent(string encodingName)
{
DbModel.Message testMessage2 = await GetTestMessage_QuotedPrintable(Encoding.GetEncoding(encodingName));
var testMessage2 = await GetTestMessage_QuotedPrintable(Encoding.GetEncoding(encodingName));
TestMessagesRepository messagesRepository = new TestMessagesRepository(testMessage2);
MessagesController messagesController = new MessagesController(messagesRepository, null);

Expand All @@ -302,7 +371,7 @@ public async Task GetMessageSource_QPMessage_ReturnsNotHeadersDecodedContent(str
public async Task GetMessageRaw_QPMessage_ReturnsHeadersAndQPContent(string encodingName)
{
var encoding = Encoding.GetEncoding(encodingName);
DbModel.Message testMessage2 = await GetTestMessage_QuotedPrintable(encoding);
var testMessage2 = await GetTestMessage_QuotedPrintable(encoding);
TestMessagesRepository messagesRepository = new TestMessagesRepository(testMessage2);
MessagesController messagesController = new MessagesController(messagesRepository, null);

Expand All @@ -315,7 +384,7 @@ public async Task GetMessageRaw_QPMessage_ReturnsHeadersAndQPContent(string enco
byte[] output = new byte[e.EstimateOutputLength(bytes.Length)];
int outputLen = e.Encode(bytes, 0, bytes.Length, output);
string qpResult = Encoding.ASCII.GetString(output, 0, outputLen);

Assert.Contains(qpResult, result);
}
}
Expand Down
6 changes: 6 additions & 0 deletions Rnwood.Smtp4dev/ApiModel/SearchMessagesCriteria.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using System;

namespace Rnwood.Smtp4dev.ApiModel
{
public record SearchMessagesCriteria(string To, string Subject, string Content, DateTime? DateFrom);
}
44 changes: 44 additions & 0 deletions Rnwood.Smtp4dev/Controllers/MessagesController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using NSwag.Annotations;
using Rnwood.Smtp4dev.Server.Settings;
using Org.BouncyCastle.Cms;
using System.Text.RegularExpressions;

namespace Rnwood.Smtp4dev.Controllers
{
Expand Down Expand Up @@ -89,6 +90,49 @@ public ApiModel.PagedResult<MessageSummary> GetSummaries(string searchTerms, str
.GetPaged(page, pageSize);
}

/// <summary>
/// Search for emails with given search terms.
/// </summary>
/// <param name="searchCriteria">Criteria to search for an email</param>
/// <param name="mailboxName">Name of the mailbox to search in</param>
/// <param name="sortColumn">Column to sort the results by</param>
/// <param name="sortIsDescending">Indicates if sorting should be in descending order</param>
/// <param name="page">Page number for pagination</param>
/// <param name="pageSize">Number of items per page</param>
/// <returns>Returns a list of message summaries including basic details but not the content</returns>
[HttpPut]
[SwaggerResponse(System.Net.HttpStatusCode.OK, typeof(ApiModel.PagedResult<MessageSummary>), Description = "")]
public ApiModel.PagedResult<MessageSummary> Find([FromBody] SearchMessagesCriteria searchCriteria, string mailboxName = MailboxOptions.DEFAULTNAME, string sortColumn = "receivedDate", bool sortIsDescending = true, int page = 1, int pageSize = 5)
{
IEnumerable<DbModel.Message> query = messagesRepository.GetMessages(mailboxName, true)
.Include(m => m.Relays)
.OrderBy(sortColumn + (sortIsDescending ? " DESC" : ""));

if (!string.IsNullOrEmpty(searchCriteria.To))
{
query = query.Where(m => m.To.Contains(searchCriteria.To, StringComparison.OrdinalIgnoreCase));
}

if (!string.IsNullOrEmpty(searchCriteria.Subject))
{
query = query.Where(m => m.Subject.Contains(searchCriteria.Subject, StringComparison.OrdinalIgnoreCase));
}

if (!string.IsNullOrEmpty(searchCriteria.Content))
{
query = query.Where(m => Regex.IsMatch(System.Text.Encoding.UTF8.GetString(m.Data), searchCriteria.Content, RegexOptions.IgnoreCase));
}

if (searchCriteria.DateFrom.HasValue)
{
query = query.Where(m => m.ReceivedDate >= searchCriteria.DateFrom);
}

return query
.Select(m => new MessageSummary(m))
.GetPaged(page, pageSize);
}

private async Task<Message> GetDbMessage(Guid id, bool tracked)
{
return (await this.messagesRepository.TryGetMessageById(id, tracked)) ??
Expand Down
9 changes: 8 additions & 1 deletion Rnwood.Smtp4dev/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,16 @@
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"SERVEROPTIONS__URLS": "http://*:5000"
"SERVEROPTIONS__URLS": "http://*:5000"
},
"applicationUrl": "http://localhost:5000/"
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
},
"iisSettings": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<VersionPrefix>3.0</VersionPrefix>
<TargetFrameworks>net6.0;net462</TargetFrameworks>
<TargetFrameworks>net8.0</TargetFrameworks>
<AssemblyName>Rnwood.SmtpServer.Tests</AssemblyName>
<OutputType>Library</OutputType>
<LangVersion>latest</LangVersion>
Expand Down