2using System.Collections.Generic;
4using System.Net.Security;
5using System.Security.Authentication;
6using System.Security.Cryptography.X509Certificates;
8using System.Threading.Tasks;
35 private readonly List<KeyValuePair<int, string>> response =
new List<KeyValuePair<int, string>>();
36 private TaskCompletionSource<KeyValuePair<int, string>[]> responseSource =
new TaskCompletionSource<KeyValuePair<int, string>[]>();
38 private readonly
object synchObj =
new object();
39 private readonly
string userName;
40 private readonly
string password;
41 private readonly
string host;
42 private readonly
int port;
43 private string domain;
44 private bool startTls =
false;
49 private bool trustCertificate =
false;
51 private string[] authMechanisms =
null;
52 private string[] permittedAuthenticationMechanisms =
null;
81 this.userName = UserName;
82 this.password = Password;
90 this.client?.Dispose();
95 this.response.Clear();
98 this.client =
new RowTcpClient(Encoding.UTF8, 10000,
false);
99 this.client.Client.ReceiveTimeout = 10000;
100 this.client.Client.SendTimeout = 10000;
102 this.client.OnReceived += this.Client_OnReceived;
103 this.client.OnSent += this.Client_OnSent;
104 this.client.OnError += this.Client_OnError;
105 this.client.OnInformation += this.Client_OnInformation;
106 this.client.OnWarning += this.Client_OnWarning;
108 await this.
Information(
"Connecting to " + this.host +
":" + this.port.ToString());
109 await this.client.ConnectAsync(this.host, this.port);
110 await this.
Information(
"Connected to " + this.host +
":" + this.port.ToString());
112 await this.AssertOkResult();
115 private async Task<string> Client_OnWarning(
string Text)
121 private async Task<string> Client_OnInformation(
string Text)
127 private Task Client_OnError(
object Sender, Exception Exception)
129 return this.
Error(Exception.Message);
132 private async Task<bool> Client_OnSent(
object Sender,
string Text)
138 private async Task<bool> Client_OnReceived(
object Sender,
string Row)
140 if (
string.IsNullOrEmpty(Row))
142 await this.
Error(
"No response returned.");
148 int i = Row.IndexOfAny(spaceHyphen);
152 if (!
int.TryParse(Row.Substring(0, i), out
int Code))
154 await this.
Error(
"Invalid response returned.");
158 bool More = i < Row.Length && Row[i] ==
'-';
163 Row = Row.Substring(i + 1).Trim();
167 this.response.Add(
new KeyValuePair<int, string>(Code, Row));
171 this.responseSource.TrySetResult(this.response.ToArray());
172 this.response.Clear();
184 this.client?.Dispose();
201 get => this.trustCertificate;
202 set => this.trustCertificate = value;
220 get => this.permittedAuthenticationMechanisms;
221 set => this.permittedAuthenticationMechanisms = value;
238 public async Task<KeyValuePair<int, string>[]>
ReadResponse(
int Timeout)
240 TaskCompletionSource<KeyValuePair<int, string>[]> Source = this.responseSource;
241 if (await Task.WhenAny(Source.Task, Task.Delay(Timeout)) != Source.Task)
242 throw new TimeoutException(
"Response not returned in time.");
244 return Source.Task.Result;
247 private static readonly
char[] spaceHyphen =
new char[] {
' ',
'-' };
249 private Task WriteLine(
string Row)
253 this.response.Clear();
254 this.responseSource =
new TaskCompletionSource<KeyValuePair<int, string>[]>();
257 return this.client.SendAsync(Row);
260 private Task Write(
byte[] Bytes)
262 return this.client.SendAsync(Bytes);
265 private Task<string> AssertOkResult()
267 return this.AssertResult(300);
270 private Task<string> AssertContinue()
272 return this.AssertResult(400);
275 private async Task<string> AssertResult(
int MaxExclusive)
277 KeyValuePair<int, string>[] Response = await this.
ReadResponse();
278 int Code = Response[0].Key;
279 string Message = Response[0].Value.Trim();
281 if (
string.IsNullOrEmpty(Message))
282 Message =
"Request rejected.";
284 if (Code < 200 || Code >= MaxExclusive)
286 if (Code >= 400 && Code < 500)
292 return Response[0].Value;
304 this.startTls =
false;
306 this.authMechanisms =
null;
312 if (
string.IsNullOrEmpty(
Domain))
313 await this.WriteLine(
"EHLO");
315 await this.WriteLine(
"EHLO " +
Domain);
317 KeyValuePair<int, string>[] Response = await this.
ReadResponse();
318 if (Response[0].Key < 200 || Response[0].Key >= 300)
319 throw new IOException(
"Request rejected.");
321 int i = Response[0].Value.LastIndexOf(
'[');
322 int j = Response[0].Value.LastIndexOf(
']');
323 string ResponseDomain;
327 ResponseDomain = Response[0].Value.Substring(i + 1, j - i - 1);
329 if (
string.IsNullOrEmpty(
Domain))
332 if (
string.IsNullOrEmpty(this.domain))
333 this.domain = ResponseDomain;
336 ResponseDomain =
string.Empty;
338 foreach (KeyValuePair<int, string> P
in Response)
340 string s = P.Value.ToUpper();
345 this.startTls =
true;
356 case "ENHANCEDSTATUSCODES":
371 if (s.StartsWith(
"AUTH "))
372 this.authMechanisms = s.Substring(5).Trim().Split(space, StringSplitOptions.RemoveEmptyEntries);
377 if (this.startTls && !(this.client.Stream is SslStream))
379 await this.WriteLine(
"STARTTLS");
380 await this.AssertOkResult();
382 await this.client.PauseReading();
385 await this.client.UpgradeToTlsAsClient(
null, SslProtocols.Tls12,
this.trustCertificate);
387 this.client.Continue();
391 else if (!(this.authMechanisms is
null) && !
string.IsNullOrEmpty(this.userName) && !
string.IsNullOrEmpty(this.password))
393 foreach (
string Mechanism
in this.authMechanisms)
395 if (Mechanism ==
"EXTERNAL")
398 SslStream SslStream = this.client.Stream as SslStream;
401 if (M.
Name != Mechanism)
407 if (!(this.permittedAuthenticationMechanisms is
null) &&
408 Array.IndexOf(
this.permittedAuthenticationMechanisms, Mechanism) < 0)
417 b = await M.
Authenticate(this.userName, this.password,
this);
427 throw new AuthenticationException(
"Unable to authenticate user.");
429 return ResponseDomain;
433 throw new AuthenticationException(
"No suitable and supported authentication mechanism found.");
436 return ResponseDomain;
439 private static readonly
char[] space =
new char[] {
' ' };
445 public async Task
VRFY(
string Account)
447 await this.WriteLine(
"VRFY " + Account);
448 await this.AssertOkResult();
457 await this.WriteLine(
"MAIL FROM: <" + Sender +
">");
458 await this.AssertOkResult();
467 await this.WriteLine(
"RCPT TO: <" + Receiver +
">");
468 await this.AssertOkResult();
476 await this.WriteLine(
"QUIT");
477 await this.AssertOkResult();
483 public async Task
DATA(KeyValuePair<string, string>[] Headers,
byte[] Body)
485 await this.WriteLine(
"DATA");
486 await this.AssertContinue();
488 foreach (KeyValuePair<string, string> Header
in Headers)
489 await this.WriteLine(Header.Key +
": " + Header.Value);
491 await this.WriteLine(
string.Empty);
499 j = this.IndexOf(Body, crLfDot, i);
503 if (i == 0 && j == c)
504 await this.Write(Body);
507 byte[] Bin =
new byte[j - i];
508 Array.Copy(Body, i, Bin, 0, j - i);
509 await this.Write(Bin);
515 await this.Write(crLfDot);
520 await this.WriteLine(
string.Empty);
521 await this.WriteLine(
string.Empty);
522 await this.WriteLine(
".");
524 await this.AssertOkResult();
527 private static readonly
byte[] crLfDot =
new byte[] { (byte)
'\r', (
byte)
'\n', (byte)
'.' };
529 private int IndexOf(
byte[] Data,
byte[] Segment,
int StartIndex)
532 int d = Segment.Length;
533 int c = Data.Length - d + 1;
535 for (i = StartIndex; i < c; i++)
537 for (j = 0; j < d; j++)
539 if (Data[i + j] != Segment[j])
558 string s =
"AUTH " + Mechanism.
Name;
559 if (!
string.IsNullOrEmpty(Parameters))
560 s +=
" " + Parameters;
562 await this.WriteLine(s);
563 return await this.AssertContinue();
574 await this.WriteLine(Parameters);
575 return await this.AssertContinue();
586 await this.WriteLine(Parameters);
587 await this.AssertOkResult();
610 ContentType =
"text/html; charset=utf-8",
611 Raw = Encoding.UTF8.GetBytes(HTML)
615 ContentType = PlainTextCodec.DefaultContentType +
"; charset=utf-8",
616 Raw = Encoding.UTF8.GetBytes(PlainText)
620 ContentType =
"text/markdown; charset=utf-8",
627 if (Attachments.Length > 0)
629 List<EmbeddedContent> Parts =
new List<EmbeddedContent>()
633 ContentType = P.Value,
638 foreach (
object Attachment
in Attachments)
643 ContentType = P2.Value,
653 byte[] BodyBin = P.Key;
654 string ContentType = P.Value;
656 List<KeyValuePair<string, string>> Headers =
new List<KeyValuePair<string, string>>()
658 new KeyValuePair<string, string>(
"MIME-VERSION",
"1.0"),
659 new KeyValuePair<string, string>(
"FROM", Sender),
660 new KeyValuePair<string, string>(
"TO", Recipient),
661 new KeyValuePair<string, string>(
"SUBJECT", Subject),
663 new KeyValuePair<string, string>(
"IMPORTANCE",
"normal"),
664 new KeyValuePair<string, string>(
"X-PRIORITY",
"3"),
665 new KeyValuePair<string, string>(
"MESSAGE-ID", Guid.NewGuid().ToString()),
666 new KeyValuePair<string, string>(
"CONTENT-TYPE", ContentType)
671 await this.
DATA(Headers.ToArray(), BodyBin);
Helps with parsing of commong data types.
static string EncodeRfc822(DateTime Timestamp)
Encodes a date and time, according to RFC 822 §5.
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< KeyValuePair< byte[], string > > 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.
Represents mixed content, encoded with multipart/mixed
Simple base class for classes implementing communication protocols.
Task Error(string Error)
Called to inform the viewer of an error state.
Task Exception(Exception Exception)
Called to inform the viewer of an exception state.
ISniffer[] Sniffers
Registered sniffers.
Task Information(string Comment)
Called to inform the viewer of something.
Task TransmitText(string Text)
Called when text has been transmitted.
Task ReceiveText(string Text)
Called when text has been received.
Task Warning(string Warning)
Called to inform the viewer of a warning state.
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
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.
Task< KeyValuePair< int, string >[]> ReadResponse()
Reads a response from the server.
async Task DATA(KeyValuePair< string, string >[] Headers, byte[] Body)
Executes the DATA command.
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.
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...