1+ // Copyright (c) Dmytro Kyshchenko. All rights reserved.
2+ // Licensed under the GPL-3.0 license. See LICENSE file in the project root for full license information.
3+
4+ using System . Buffers ;
5+ using System . Collections . Concurrent ;
6+ using System . Text . RegularExpressions ;
7+ using SmtpServer ;
8+ using SmtpServer . Authentication ;
9+ using SmtpServer . ComponentModel ;
10+ using SmtpServer . Protocol ;
11+ using SmtpServer . Storage ;
12+
13+ namespace Pyro . ApiTests . Clients ;
14+
15+ internal sealed partial class Smtp : IMessageStore
16+ {
17+ private readonly SmtpServer . SmtpServer smtpServer ;
18+ private readonly BlockingCollection < Message > messages ;
19+
20+ public Smtp ( )
21+ {
22+ var options = new SmtpServerOptionsBuilder ( )
23+ . ServerName ( "localhost" )
24+ . Endpoint ( builder => builder
25+ . Port ( 25 , false )
26+ . AllowUnsecureAuthentication ( ) )
27+ . Build ( ) ;
28+
29+ var serviceProvider = new ServiceProvider ( ) ;
30+ serviceProvider . Add ( this ) ;
31+ serviceProvider . Add ( MailboxFilter . Default ) ;
32+ serviceProvider . Add ( UserAuthenticator . Default ) ;
33+
34+ smtpServer = new SmtpServer . SmtpServer ( options , serviceProvider ) ;
35+ messages = [ ] ;
36+ }
37+
38+ public void Start ( )
39+ => Task . Run ( ( ) => smtpServer . StartAsync ( CancellationToken . None ) )
40+ . ContinueWith (
41+ task => Console . WriteLine ( task . Exception ) ,
42+ CancellationToken . None ,
43+ TaskContinuationOptions . OnlyOnFaulted ,
44+ TaskScheduler . Default ) ;
45+
46+ public void Stop ( )
47+ => smtpServer . Shutdown ( ) ;
48+
49+ public async Task < SmtpResponse > SaveAsync (
50+ ISessionContext context ,
51+ IMessageTransaction transaction ,
52+ ReadOnlySequence < byte > buffer ,
53+ CancellationToken cancellationToken )
54+ {
55+ await using var stream = new MemoryStream ( ) ;
56+
57+ var position = buffer . GetPosition ( 0 ) ;
58+ while ( buffer . TryGet ( ref position , out var memory ) )
59+ await stream . WriteAsync ( memory , cancellationToken ) ;
60+
61+ stream . Position = 0 ;
62+
63+ var message = await MimeKit . MimeMessage . LoadAsync ( stream , cancellationToken ) ;
64+ messages . Add (
65+ new Message
66+ {
67+ From = message . From . Mailboxes . First ( ) . Address ,
68+ To = message . To . Mailboxes . First ( ) . Address ,
69+ Body = message . HtmlBody ,
70+ } ,
71+ cancellationToken ) ;
72+
73+ return SmtpResponse . Ok ;
74+ }
75+
76+ public Message ? WaitForMessage ( Func < Message , bool > condition , CancellationToken cancellationToken = default )
77+ {
78+ using var timeout = new CancellationTokenSource ( TimeSpan . FromMinutes ( 1 ) ) ;
79+ using var linked = CancellationTokenSource . CreateLinkedTokenSource ( cancellationToken , timeout . Token ) ;
80+
81+ foreach ( var message in messages . GetConsumingEnumerable ( linked . Token ) )
82+ if ( condition ( message ) )
83+ return message ;
84+
85+ return null ;
86+ }
87+
88+ public partial class Message
89+ {
90+ public required string From { get ; set ; }
91+
92+ public required string To { get ; set ; }
93+
94+ public required string Body { get ; set ; }
95+
96+ [ GeneratedRegex ( "token=(.*)\" " ) ]
97+ private static partial Regex TokenRegex ( ) ;
98+
99+ public string GetToken ( )
100+ => Uri . UnescapeDataString ( TokenRegex ( ) . Match ( Body ) . Groups [ 1 ] . Value ) ;
101+ }
102+ }
0 commit comments