5using System.Security.Authentication;
6using System.Security.Cryptography.X509Certificates;
8using System.Threading.Tasks;
37 private readonly List<KeyValuePair<int, string>> response =
new List<KeyValuePair<int, string>>();
38 private TaskCompletionSource<KeyValuePair<int, string>[]> responseSource =
new TaskCompletionSource<KeyValuePair<int, string>[]>();
40 private readonly
object synchObj =
new object();
41 private readonly
string userName;
42 private readonly
string password;
43 private readonly
string host;
44 private readonly
int port;
45 private string domain;
46 private bool startTls =
false;
51 private bool trustCertificate =
false;
53 private string[] authMechanisms =
null;
54 private string[] permittedAuthenticationMechanisms =
null;
83 this.userName = UserName;
84 this.password = Password;
92 if (!(this.client is
null))
94 await this.client.DisposeAsync();
100 this.response.Clear();
103 this.client =
new RowTcpClient(Encoding.UTF8, 10000,
false);
104 this.client.Client.ReceiveTimeout = 10000;
105 this.client.Client.SendTimeout = 10000;
107 this.client.OnReceived += this.Client_OnReceived;
108 this.client.OnSent += this.Client_OnSent;
109 this.client.OnError += this.Client_OnError;
110 this.client.OnInformation += this.Client_OnInformation;
111 this.client.OnWarning += this.Client_OnWarning;
113 this.
Information(
"Connecting to " + this.host +
":" + this.port.ToString());
114 await this.client.ConnectAsync(this.host, this.port);
115 this.
Information(
"Connected to " + this.host +
":" + this.port.ToString());
117 await this.AssertOkResult();
120 private string Client_OnWarning(
string Text)
126 private string Client_OnInformation(
string Text)
132 private Task Client_OnError(
object Sender, Exception Exception)
134 this.
Error(Exception.Message);
135 return Task.CompletedTask;
138 private Task<bool> Client_OnSent(
object Sender,
string Text)
141 return Task.FromResult(
true);
144 private Task<bool> Client_OnReceived(
object Sender,
string Row)
146 if (
string.IsNullOrEmpty(Row))
148 this.
Error(
"No response returned.");
149 return Task.FromResult(
true);
154 int i = Row.IndexOfAny(spaceHyphen);
158 if (!
int.TryParse(Row[..i], out
int Code))
160 this.
Error(
"Invalid response returned.");
161 return Task.FromResult(
true);
164 bool More = i < Row.Length && Row[i] ==
'-';
169 Row = Row[(i + 1)..].Trim();
173 this.response.Add(
new KeyValuePair<int, string>(Code, Row));
177 this.responseSource.TrySetResult(this.response.ToArray());
178 this.response.Clear();
182 return Task.FromResult(
true);
188 [Obsolete(
"Use DisposeAsync() instead.")]
199 if (!(this.client is
null))
201 await this.client.DisposeAsync();
216 get => this.trustCertificate;
217 set => this.trustCertificate = value;
235 get => this.permittedAuthenticationMechanisms;
236 set => this.permittedAuthenticationMechanisms = value;
253 public async Task<KeyValuePair<int, string>[]>
ReadResponse(
int Timeout)
255 TaskCompletionSource<KeyValuePair<int, string>[]> Source = this.responseSource;
256 if (await Task.WhenAny(Source.Task, Task.Delay(Timeout)) != Source.Task)
257 throw new TimeoutException(
"Response not returned in time.");
259 return Source.Task.Result;
262 private static readonly
char[] spaceHyphen =
new char[] {
' ',
'-' };
264 private Task WriteLine(
string Row)
268 this.response.Clear();
269 this.responseSource =
new TaskCompletionSource<KeyValuePair<int, string>[]>();
272 return this.client.SendAsync(Row);
275 private Task Write(
bool ConstantBuffer,
byte[] Bytes)
277 return this.client.SendAsync(ConstantBuffer, Bytes);
280 private Task<string> AssertOkResult()
282 return this.AssertResult(300);
285 private Task<string> AssertContinue()
287 return this.AssertResult(400);
290 private async Task<string> AssertResult(
int MaxExclusive)
292 KeyValuePair<int, string>[] Response = await this.
ReadResponse();
293 int Code = Response[0].Key;
294 string Message = Response[0].Value.Trim();
296 if (
string.IsNullOrEmpty(Message))
297 Message =
"Request rejected.";
299 if (Code < 200 || Code >= MaxExclusive)
301 if (Code >= 400 && Code < 500)
307 return Response[0].Value;
319 this.startTls =
false;
321 this.authMechanisms =
null;
327 if (
string.IsNullOrEmpty(
Domain))
328 await this.WriteLine(
"EHLO");
330 await this.WriteLine(
"EHLO " +
Domain);
332 KeyValuePair<int, string>[] Response = await this.
ReadResponse();
333 if (Response[0].Key < 200 || Response[0].Key >= 300)
334 throw new IOException(
"Request rejected.");
336 int i = Response[0].Value.LastIndexOf(
'[');
337 int j = Response[0].Value.LastIndexOf(
']');
338 string ResponseDomain;
342 ResponseDomain = Response[0].Value.Substring(i + 1, j - i - 1);
344 if (
string.IsNullOrEmpty(
Domain))
347 if (
string.IsNullOrEmpty(this.domain))
348 this.domain = ResponseDomain;
351 ResponseDomain =
string.Empty;
353 foreach (KeyValuePair<int, string> P
in Response)
355 string s = P.Value.ToUpper();
360 this.startTls =
true;
371 case "ENHANCEDSTATUSCODES":
386 if (s.StartsWith(
"AUTH "))
387 this.authMechanisms = s[5..].Trim().Split(space, StringSplitOptions.RemoveEmptyEntries);
392 if (this.startTls && !(this.client.Stream is SslStream))
394 await this.WriteLine(
"STARTTLS");
395 await this.AssertOkResult();
397 await this.client.PauseReading();
400 await this.client.UpgradeToTlsAsClient(
null,
Crypto.
SecureTls,
this.trustCertificate);
402 this.client.Continue();
404 ResponseDomain = await this.
EHLO(Domain);
406 else if (!(this.authMechanisms is
null) && !
string.IsNullOrEmpty(this.userName) && !
string.IsNullOrEmpty(this.password))
408 foreach (
string Mechanism
in this.authMechanisms)
410 if (Mechanism ==
"EXTERNAL")
413 SslStream SslStream = this.client.Stream as SslStream;
416 if (M.
Name != Mechanism)
422 if (!(this.permittedAuthenticationMechanisms is
null) &&
423 Array.IndexOf(
this.permittedAuthenticationMechanisms, Mechanism) < 0)
432 b = await M.
Authenticate(this.userName, this.password,
this);
442 throw new AuthenticationException(
"Unable to authenticate user.");
444 return ResponseDomain;
448 throw new AuthenticationException(
"No suitable and supported authentication mechanism found.");
451 return ResponseDomain;
454 private static readonly
char[] space =
new char[] {
' ' };
460 public async Task
VRFY(
string Account)
462 await this.WriteLine(
"VRFY " + Account);
463 await this.AssertOkResult();
472 await this.WriteLine(
"MAIL FROM: <" + Sender +
">");
473 await this.AssertOkResult();
482 await this.WriteLine(
"RCPT TO: <" + Receiver +
">");
483 await this.AssertOkResult();
491 await this.WriteLine(
"QUIT");
492 await this.AssertOkResult();
498 public Task
DATA(KeyValuePair<string, string>[] Headers,
byte[] Body)
500 return this.
DATA(Headers,
false, Body);
506 public async Task
DATA(KeyValuePair<string, string>[] Headers,
bool ConstantBody,
byte[] Body)
508 await this.WriteLine(
"DATA");
509 await this.AssertContinue();
511 foreach (KeyValuePair<string, string> Header
in Headers)
512 await this.WriteLine(Header.Key +
": " + Header.Value);
514 await this.WriteLine(
string.Empty);
522 j = this.IndexOf(Body, crLfDot, i);
526 if (i == 0 && j == c)
527 await this.Write(ConstantBody, Body);
530 byte[] Bin =
new byte[j - i];
531 Buffer.BlockCopy(Body, i, Bin, 0, j - i);
532 await this.Write(
true, Bin);
538 await this.Write(
true, crLfDot);
543 await this.WriteLine(
string.Empty);
544 await this.WriteLine(
string.Empty);
545 await this.WriteLine(
".");
547 await this.AssertOkResult();
550 private static readonly
byte[] crLfDot =
new byte[] { (byte)
'\r', (
byte)
'\n', (byte)
'.' };
552 private int IndexOf(
byte[] Data,
byte[] Segment,
int StartIndex)
555 int d = Segment.Length;
556 int c = Data.Length - d + 1;
558 for (i = StartIndex; i < c; i++)
560 for (j = 0; j < d; j++)
562 if (Data[i + j] != Segment[j])
581 string s =
"AUTH " + Mechanism.
Name;
582 if (!
string.IsNullOrEmpty(Parameters))
583 s +=
" " + Parameters;
585 await this.WriteLine(s);
586 return await this.AssertContinue();
597 await this.WriteLine(Parameters);
598 return await this.AssertContinue();
609 await this.WriteLine(Parameters);
610 await this.AssertOkResult();
633 ContentType =
"text/html; charset=utf-8",
634 Raw = Encoding.UTF8.GetBytes(HTML)
638 ContentType = PlainTextCodec.DefaultContentType +
"; charset=utf-8",
639 Raw = Encoding.UTF8.GetBytes(PlainText)
643 ContentType =
"text/markdown; charset=utf-8",
650 if (Attachments.Length > 0)
652 List<EmbeddedContent> Parts =
new List<EmbeddedContent>()
656 ContentType = P.ContentType,
661 foreach (
object Attachment
in Attachments)
669 byte[] BodyBin = P.Encoded;
670 string ContentType = P.ContentType;
672 List<KeyValuePair<string, string>> Headers =
new List<KeyValuePair<string, string>>()
674 new KeyValuePair<string, string>(
"MIME-VERSION",
"1.0"),
675 new KeyValuePair<string, string>(
"FROM", Sender),
676 new KeyValuePair<string, string>(
"TO", Recipient),
677 new KeyValuePair<string, string>(
"SUBJECT", Subject),
679 new KeyValuePair<string, string>(
"IMPORTANCE",
"normal"),
680 new KeyValuePair<string, string>(
"X-PRIORITY",
"3"),
681 new KeyValuePair<string, string>(
"MESSAGE-ID", Guid.NewGuid().ToString()),
682 new KeyValuePair<string, string>(
"CONTENT-TYPE", ContentType)
687 await this.
DATA(Headers.ToArray(),
true, BodyBin);
Helps with parsing of commong data types.
static string EncodeRfc822(DateTime Timestamp)
Encodes a date and time, according to RFC 822 §5.
Contains information about a response to a content request.
static string GetBody(string Html)
Extracts the contents of the BODY element in a HTML string.
Static class managing encoding and decoding of internet content.
static Task< ContentResponse > EncodeAsync(object Object, Encoding Encoding, params string[] AcceptedContentTypes)
Encodes an object.
Class that can be used to encapsulate Markdown to be returned from a Web Service, bypassing any encod...
Contains a markdown document. This markdown document class supports original markdown,...
async Task< string > GeneratePlainText()
Generates Plain Text from the markdown text.
async Task< string > GenerateHTML()
Generates HTML from the markdown text.
static Task< MarkdownDocument > CreateAsync(string MarkdownText, params Type[] TransparentExceptionTypes)
Contains a markdown document. This markdown document class supports original markdown,...
Represents alternative versions of the same content, encoded with multipart/alternative
Represents content embedded in other content.
static Task< EmbeddedContent > Encode(object Content)
Encodes an object into an embedded content.
Represents mixed content, encoded with multipart/mixed
Simple base class for classes implementing communication protocols.
void TransmitText(string Text)
Called when text has been transmitted.
void Exception(Exception Exception)
Called to inform the viewer of an exception state.
void ReceiveText(string Text)
Called when text has been received.
ISniffer[] Sniffers
Registered sniffers.
void Error(string Error)
Called to inform the viewer of an error state.
void Warning(string Warning)
Called to inform the viewer of a warning state.
void Information(string Comment)
Called to inform the viewer of something.
Implements a text-based TCP Client, by using the thread-safe full-duplex BinaryTcpClient....
Module maintaining available SASL mechanisms.
static IAuthenticationMechanism[] Mechanisms
Available SASL mechanisms.
Base class for SMTP-related exceptions.
Base class for temporary SMTP-related exceptions.
async Task< string > ChallengeResponse(IAuthenticationMechanism Mechanism, string Parameters)
Sends a challenge response back to the server.
SimpleSmtpClient(string Domain, string Host, int Port, string UserName, string Password, params ISniffer[] Sniffers)
Simple SMTP Client
Task DATA(KeyValuePair< string, string >[] Headers, byte[] Body)
Executes the DATA command.
bool TrustCertificate
If server certificate should be trusted by default (default=false).
const int DefaultSmtpPort
25
async Task Connect()
Connects to the server.
async Task< string > EHLO(string Domain)
Sends the EHLO command.
async Task< string > Initiate(IAuthenticationMechanism Mechanism, string Parameters)
Initiates authentication
async Task QUIT()
Executes the QUIT command.
async Task MAIL_FROM(string Sender)
Executes the MAIL FROM command.
async Task< string > FinalResponse(IAuthenticationMechanism Mechanism, string Parameters)
Sends a final response back to the server.
void Dispose()
Disposes of the client.
async Task VRFY(string Account)
Executes the VRFY command.
string[] PermittedAuthenticationMechanisms
Permitted authentication mechanisms.
X509Certificate ServerCertificate
Server certificate.
bool ServerCertificateValid
If server certificate is valid.
async Task DATA(KeyValuePair< string, string >[] Headers, bool ConstantBody, byte[] Body)
Executes the DATA command.
Task< KeyValuePair< int, string >[]> ReadResponse()
Reads a response from the server.
async Task DisposeAsync()
IDisposableAsync.DisposeAsync
async Task SendFormattedEMail(string Sender, string Recipient, string Subject, string MarkdownContent, params object[] Attachments)
Sends a formatted e-mail message.
async Task RCPT_TO(string Receiver)
Executes the RCPT TO command.
SimpleSmtpClient(string Domain, string Host, int Port, params ISniffer[] Sniffers)
Simple SMTP Client
const int AlternativeSmtpPort
587
async Task< KeyValuePair< int, string >[]> ReadResponse(int Timeout)
Reads a response from the server.
Helper methods for encrypting and decrypting streams of data.
const SslProtocols SecureTls
TLS 1.2 & 1.3
Interface for asynchronously disposable objects.
Interface for authentication mechanisms.
Task< bool?> Authenticate(string UserName, string Password, ISaslClientSide Connection)
Authenticates the user using the provided credentials.
bool Allowed(SslStream SslStream)
Checks if a mechanism is allowed during the current conditions.
string Name
Name of the mechanism.
Interface for client-side client connections.
Interface for sniffers. Sniffers can be added to ICommunicationLayer classes to eavesdrop on communic...