Skip to content

Commit 78350fc

Browse files
Copilotrnwoodweb-flow
authored
feat: extend message search to additionally search message body, attachment names and cc. (#1864)
* feat: implement enhanced message search for CC, body content, and attachment filenames Co-authored-by: rnwood <[email protected]> * feat: implement database-backed MIME metadata search for enhanced performance Co-authored-by: rnwood <[email protected]> * refactor: consolidate MIME processing logic and make metadata population synchronous during startup Co-authored-by: rnwood <[email protected]> * fix: update test constructors to include required MimeProcessingService dependency Co-authored-by: rnwood <[email protected]> * Fix merge issues * Fix refresh --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: rnwood <[email protected]> Co-authored-by: smtp4dev-automation <[email protected]>
1 parent d50f5bf commit 78350fc

18 files changed

Lines changed: 698 additions & 38 deletions

Rnwood.Smtp4dev.Tests/Controllers/MessagesControllerTests.cs

Lines changed: 89 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public async Task GetMessage_ValidMime()
3131
DbModel.Message testMessage1 = await GetTestMessage1();
3232

3333
TestMessagesRepository messagesRepository = new TestMessagesRepository(testMessage1);
34-
MessagesController messagesController = new MessagesController(messagesRepository, null);
34+
MessagesController messagesController = new MessagesController(messagesRepository, null, new MimeProcessingService());
3535

3636
ApiModel.Message result = await messagesController.GetMessage(testMessage1.Id);
3737

@@ -101,20 +101,38 @@ public async Task GetMessage_ValidMime()
101101

102102
IMessage message = await memoryMessageBuilder.ToMessage();
103103

104-
var dbMessage = await new MessageConverter().ConvertAsync(message, ["[email protected]"]);
104+
var dbMessage = await new MessageConverter(new MimeProcessingService()).ConvertAsync(message, ["[email protected]"]);
105105
return dbMessage;
106106
}
107107

108108
private static async Task<DbModel.Message> GetTestMessage(string subject, string from = "[email protected]", string to = "[email protected]")
109+
{
110+
return await GetTestMessageWithExtras(subject, from, to, null, null, null, null);
111+
}
112+
113+
private static async Task<DbModel.Message> GetTestMessageWithExtras(string subject, string from = "[email protected]", string to = "[email protected]",
114+
string cc = null, string htmlBody = null, string textBody = null, string attachmentFileName = null)
109115
{
110116
MimeMessage mimeMessage = new MimeMessage();
111117
mimeMessage.From.Add(InternetAddress.Parse(from));
112118
mimeMessage.To.Add(InternetAddress.Parse(to));
119+
120+
if (!string.IsNullOrEmpty(cc))
121+
{
122+
mimeMessage.Cc.Add(InternetAddress.Parse(cc));
123+
}
113124

114125
mimeMessage.Subject = subject;
115126
BodyBuilder bodyBuilder = new BodyBuilder();
116-
bodyBuilder.HtmlBody = "<html>Hi</html>";
117-
bodyBuilder.TextBody = "Hi";
127+
bodyBuilder.HtmlBody = htmlBody ?? "<html>Hi</html>";
128+
bodyBuilder.TextBody = textBody ?? "Hi";
129+
130+
if (!string.IsNullOrEmpty(attachmentFileName))
131+
{
132+
bodyBuilder.Attachments.Add(attachmentFileName,
133+
new MemoryStream(System.Text.Encoding.UTF8.GetBytes("Test attachment content")),
134+
new ContentType("text", "plain"));
135+
}
118136

119137
mimeMessage.Body = bodyBuilder.ToMessageBody();
120138

@@ -129,7 +147,7 @@ public async Task GetMessage_ValidMime()
129147

130148
IMessage message = await memoryMessageBuilder.ToMessage();
131149

132-
var dbMessage = await new MessageConverter().ConvertAsync(message, [to]);
150+
var dbMessage = await new MessageConverter(new MimeProcessingService()).ConvertAsync(message, [to]);
133151
dbMessage.Mailbox = new DbModel.Mailbox { Name = MailboxOptions.DEFAULTNAME };
134152
dbMessage.MailboxFolder = new DbModel.MailboxFolder { Name = MailboxFolder.INBOX, Mailbox = dbMessage.Mailbox };
135153

@@ -161,7 +179,7 @@ public async Task GetMessage_ValidMime()
161179

162180
IMessage message = await memoryMessageBuilder.ToMessage();
163181

164-
var dbMessage = await new MessageConverter().ConvertAsync(message, ["[email protected]"]);
182+
var dbMessage = await new MessageConverter(new MimeProcessingService()).ConvertAsync(message, ["[email protected]"]);
165183
return dbMessage;
166184
}
167185

@@ -172,7 +190,7 @@ public async Task GetSummaries_NoSearch_AllMessagesReturned()
172190
DbModel.Message testMessage2 = await GetTestMessage("Message subject2");
173191
DbModel.Message testMessage3 = await GetTestMessage("Message subject3");
174192
TestMessagesRepository messagesRepository = new TestMessagesRepository(testMessage1, testMessage2, testMessage3);
175-
MessagesController messagesController = new MessagesController(messagesRepository, null);
193+
MessagesController messagesController = new MessagesController(messagesRepository, null, new MimeProcessingService());
176194

177195
var result = messagesController.GetSummaries(null);
178196
result.Results.Select(m => m.Id).Should().BeEquivalentTo(new[] { testMessage1.Id, testMessage2.Id, testMessage3.Id });
@@ -190,18 +208,72 @@ public async Task GetSummaries_Search_MatchingMessagesReturned()
190208
new MessagesRepository(Substitute.For<ITaskQueue>(), Substitute.For<NotificationsHub>(), context);
191209
messagesRepository.DbContext.Messages.AddRange(testMessage1, testMessage2, testMessage3);
192210
await messagesRepository.DbContext.SaveChangesAsync();
193-
MessagesController messagesController = new MessagesController(messagesRepository, null);
211+
MessagesController messagesController = new MessagesController(messagesRepository, null, new MimeProcessingService());
194212

195213
var result = messagesController.GetSummaries("sUbJect2");
196214
result.Results.Select(m => m.Id).Should().BeEquivalentTo(new[] { testMessage2.Id });
197215
}
198216

217+
[Fact]
218+
public async Task GetSummaries_SearchInCC_MatchingMessagesReturned()
219+
{
220+
DbModel.Message testMessage1 = await GetTestMessageWithExtras("Subject1", cc: "[email protected]");
221+
DbModel.Message testMessage2 = await GetTestMessageWithExtras("Subject2", cc: "[email protected]");
222+
DbModel.Message testMessage3 = await GetTestMessage("Subject3");
223+
var sqlLiteForTesting = new SqliteInMemory();
224+
var context = new Smtp4devDbContext(sqlLiteForTesting.ContextOptions);
225+
MessagesRepository messagesRepository =
226+
new MessagesRepository(Substitute.For<ITaskQueue>(), Substitute.For<NotificationsHub>(), context);
227+
messagesRepository.DbContext.Messages.AddRange(testMessage1, testMessage2, testMessage3);
228+
await messagesRepository.DbContext.SaveChangesAsync();
229+
MessagesController messagesController = new MessagesController(messagesRepository, null, new MimeProcessingService());
230+
231+
var result = messagesController.GetSummaries("ccuser");
232+
result.Results.Select(m => m.Id).Should().BeEquivalentTo(new[] { testMessage1.Id });
233+
}
234+
235+
[Fact]
236+
public async Task GetSummaries_SearchInBodyContent_MatchingMessagesReturned()
237+
{
238+
DbModel.Message testMessage1 = await GetTestMessageWithExtras("Subject1", htmlBody: "<html>Unique search content here</html>", textBody: "Plain text");
239+
DbModel.Message testMessage2 = await GetTestMessageWithExtras("Subject2", htmlBody: "<html>Different content</html>", textBody: "Also different");
240+
DbModel.Message testMessage3 = await GetTestMessageWithExtras("Subject3", htmlBody: "<html>Normal</html>", textBody: "Unique search content here in plain text");
241+
var sqlLiteForTesting = new SqliteInMemory();
242+
var context = new Smtp4devDbContext(sqlLiteForTesting.ContextOptions);
243+
MessagesRepository messagesRepository =
244+
new MessagesRepository(Substitute.For<ITaskQueue>(), Substitute.For<NotificationsHub>(), context);
245+
messagesRepository.DbContext.Messages.AddRange(testMessage1, testMessage2, testMessage3);
246+
await messagesRepository.DbContext.SaveChangesAsync();
247+
MessagesController messagesController = new MessagesController(messagesRepository, null, new MimeProcessingService());
248+
249+
var result = messagesController.GetSummaries("Unique search content");
250+
result.Results.Select(m => m.Id).Should().BeEquivalentTo(new[] { testMessage1.Id, testMessage3.Id });
251+
}
252+
253+
[Fact]
254+
public async Task GetSummaries_SearchInAttachmentFilenames_MatchingMessagesReturned()
255+
{
256+
DbModel.Message testMessage1 = await GetTestMessageWithExtras("Subject1", attachmentFileName: "important-document.pdf");
257+
DbModel.Message testMessage2 = await GetTestMessageWithExtras("Subject2", attachmentFileName: "regular-file.txt");
258+
DbModel.Message testMessage3 = await GetTestMessage("Subject3");
259+
var sqlLiteForTesting = new SqliteInMemory();
260+
var context = new Smtp4devDbContext(sqlLiteForTesting.ContextOptions);
261+
MessagesRepository messagesRepository =
262+
new MessagesRepository(Substitute.For<ITaskQueue>(), Substitute.For<NotificationsHub>(), context);
263+
messagesRepository.DbContext.Messages.AddRange(testMessage1, testMessage2, testMessage3);
264+
await messagesRepository.DbContext.SaveChangesAsync();
265+
MessagesController messagesController = new MessagesController(messagesRepository, null, new MimeProcessingService());
266+
267+
var result = messagesController.GetSummaries("important-document");
268+
result.Results.Select(m => m.Id).Should().BeEquivalentTo(new[] { testMessage1.Id });
269+
}
270+
199271
[Fact]
200272
public async Task GetHtmlBody()
201273
{
202274
DbModel.Message testMessage1 = await GetTestMessage1();
203275
TestMessagesRepository messagesRepository = new TestMessagesRepository(testMessage1);
204-
MessagesController messagesController = new MessagesController(messagesRepository, null);
276+
MessagesController messagesController = new MessagesController(messagesRepository, null, new MimeProcessingService());
205277

206278
var result = await messagesController.GetMessageHtml(testMessage1.Id);
207279
Assert.Equal(message1HtmlBody, result.Value);
@@ -212,7 +284,7 @@ public async Task GetTextBody()
212284
{
213285
DbModel.Message testMessage1 = await GetTestMessage1();
214286
TestMessagesRepository messagesRepository = new TestMessagesRepository(testMessage1);
215-
MessagesController messagesController = new MessagesController(messagesRepository, null);
287+
MessagesController messagesController = new MessagesController(messagesRepository, null, new MimeProcessingService());
216288

217289
string text = (await messagesController.GetMessagePlainText(testMessage1.Id)).Value;
218290
Assert.Equal(message1TextBody, text);
@@ -223,7 +295,7 @@ public async Task GetHtmlBody_WhenThereIsntOne_ReturnsNotFound()
223295
{
224296
DbModel.Message testMessage1 = await GetTestMessage1(includeHtmlBody:false);
225297
TestMessagesRepository messagesRepository = new TestMessagesRepository(testMessage1);
226-
MessagesController messagesController = new MessagesController(messagesRepository, null);
298+
MessagesController messagesController = new MessagesController(messagesRepository, null, new MimeProcessingService());
227299

228300
var result = await messagesController.GetMessageHtml(testMessage1.Id);
229301
Assert.IsType<NotFoundObjectResult>(result.Result);
@@ -234,7 +306,7 @@ public async Task GetTextBody_WhenThereIsntOne_ReturnsNotFound()
234306
{
235307
DbModel.Message testMessage1 = await GetTestMessage1(includeTextBody:false);
236308
TestMessagesRepository messagesRepository = new TestMessagesRepository(testMessage1);
237-
MessagesController messagesController = new MessagesController(messagesRepository, null);
309+
MessagesController messagesController = new MessagesController(messagesRepository, null, new MimeProcessingService());
238310

239311
var result= await messagesController.GetMessagePlainText(testMessage1.Id);
240312
Assert.IsType<NotFoundObjectResult>(result.Result);
@@ -247,7 +319,7 @@ public async Task GetNewSummaries_NoBookmark_AllMessagesReturned()
247319
DbModel.Message testMessage3 = await GetTestMessage1();
248320
TestMessagesRepository messagesRepository = new TestMessagesRepository(testMessage1, testMessage2, testMessage3);
249321

250-
MessagesController messagesController = new MessagesController(messagesRepository, null);
322+
MessagesController messagesController = new MessagesController(messagesRepository, null, new MimeProcessingService());
251323

252324
var result = messagesController.GetNewSummaries(null);
253325
result.Select(m => m.Id).Should().BeEquivalentTo(new[] { testMessage1.Id, testMessage2.Id, testMessage3.Id });
@@ -260,7 +332,7 @@ public async Task GetNewSummaries_NoBookmark_NewerMessagesReturned()
260332
DbModel.Message testMessage2 = await GetTestMessage1();
261333
DbModel.Message testMessage3 = await GetTestMessage1();
262334
TestMessagesRepository messagesRepository = new TestMessagesRepository(testMessage1, testMessage2, testMessage3);
263-
MessagesController messagesController = new MessagesController(messagesRepository, null);
335+
MessagesController messagesController = new MessagesController(messagesRepository, null, new MimeProcessingService());
264336

265337
var result = messagesController.GetNewSummaries(testMessage2.Id);
266338
result.Select(m => m.Id).Should().BeEquivalentTo(new[] { testMessage3.Id });
@@ -271,7 +343,7 @@ public async Task GetPartContent()
271343
{
272344
DbModel.Message testMessage1 = await GetTestMessage1();
273345
TestMessagesRepository messagesRepository = new TestMessagesRepository(testMessage1);
274-
MessagesController messagesController = new MessagesController(messagesRepository, null);
346+
MessagesController messagesController = new MessagesController(messagesRepository, null, new MimeProcessingService());
275347

276348
var parts = (await messagesController.GetMessage(testMessage1.Id)).Parts.Flatten(p => p.ChildParts).SelectMany(p => p.Attachments);
277349

@@ -290,7 +362,7 @@ public async Task GetMessageSource_QPMessage_ReturnsNotHeadersDecodedContent(str
290362
{
291363
DbModel.Message testMessage2 = await GetTestMessage_QuotedPrintable(Encoding.GetEncoding(encodingName));
292364
TestMessagesRepository messagesRepository = new TestMessagesRepository(testMessage2);
293-
MessagesController messagesController = new MessagesController(messagesRepository, null);
365+
MessagesController messagesController = new MessagesController(messagesRepository, null, new MimeProcessingService());
294366

295367

296368
string result = await messagesController.GetMessageSource(testMessage2.Id);
@@ -306,7 +378,7 @@ public async Task GetMessageRaw_QPMessage_ReturnsHeadersAndQPContent(string enco
306378
var encoding = Encoding.GetEncoding(encodingName);
307379
DbModel.Message testMessage2 = await GetTestMessage_QuotedPrintable(encoding);
308380
TestMessagesRepository messagesRepository = new TestMessagesRepository(testMessage2);
309-
MessagesController messagesController = new MessagesController(messagesRepository, null);
381+
MessagesController messagesController = new MessagesController(messagesRepository, null, new MimeProcessingService());
310382

311383

312384
string result = await messagesController.GetMessageSourceRaw(testMessage2.Id);

Rnwood.Smtp4dev.Tests/Controllers/RelayMessageTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public RelayMessagesTests()
2828
{
2929
messagesRepository = Substitute.For<IMessagesRepository>();
3030
server = Substitute.For<ISmtp4devServer>();
31-
controller = new MessagesController(messagesRepository, server);
31+
controller = new MessagesController(messagesRepository, server, new MimeProcessingService());
3232
var sqlLiteForTesting = new SqliteInMemory();
3333
context = new Smtp4devDbContext(sqlLiteForTesting.ContextOptions);
3434
InitRepo();

Rnwood.Smtp4dev.Tests/Data/DataModelTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public async Task CanDeleteSessionWhenMessageExist()
5959

6060
IMessage message = await memoryMessageBuilder.ToMessage();
6161

62-
var dbMessage = await new MessageConverter().ConvertAsync(message, [to]);
62+
var dbMessage = await new MessageConverter(new MimeProcessingService()).ConvertAsync(message, [to]);
6363
dbMessage.Mailbox = new DbModel.Mailbox { Name = MailboxOptions.DEFAULTNAME };
6464

6565
return dbMessage;

Rnwood.Smtp4dev.Tests/ImapSearchTranslatorTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ public async Task Older()
362362

363363
IMessage message = await memoryMessageBuilder.ToMessage();
364364

365-
var dbMessage = await new MessageConverter().ConvertAsync(message, [to]);
365+
var dbMessage = await new MessageConverter(new MimeProcessingService()).ConvertAsync(message, [to]);
366366
dbMessage.Mailbox = new DbModel.Mailbox { Name = MailboxOptions.DEFAULTNAME };
367367
dbMessage.IsUnread = unread;
368368

Rnwood.Smtp4dev/ClientApp/src/components/messagelist.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -469,7 +469,10 @@
469469
}
470470
471471
@Watch("searchTerm")
472-
doSearch = debounce(() => this.refresh(false), 200);
472+
onSearchTermChanged() {
473+
this.debouncedDoSearch();
474+
}
475+
debouncedDoSearch = debounce(() => this.refresh(false), 200);
473476
474477
@Watch("selectedMailbox")
475478
async onMailboxChanged() {

0 commit comments

Comments
 (0)