Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
SimpleSmtpClient.cs
1using System;
3using System.IO;
5using System.Security.Authentication;
6using System.Security.Cryptography.X509Certificates;
7using System.Text;
8using System.Threading.Tasks;
9using Waher.Content;
14using Waher.Events;
18using Waher.Security;
19
21{
26 {
30 public const int DefaultSmtpPort = 25;
31
35 public const int AlternativeSmtpPort = 587;
36
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>[]>();
39 private RowTcpClient client;
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;
47 //private bool smptUtf8 = false;
48 //private bool eightBitMime = false;
49 //private bool enhancedStatusCodes = false;
50 //private bool help = false;
51 private bool trustCertificate = false;
52 //private int? size = null;
53 private string[] authMechanisms = null;
54 private string[] permittedAuthenticationMechanisms = null;
55
63 public SimpleSmtpClient(string Domain, string Host, int Port, params ISniffer[] Sniffers)
64 : this(Domain, Host, Port, null, null, Sniffers)
65 {
66 }
67
77 public SimpleSmtpClient(string Domain, string Host, int Port, string UserName, string Password, params ISniffer[] Sniffers)
78 : base(false, Sniffers)
79 {
80 this.domain = Domain;
81 this.host = Host;
82 this.port = Port;
83 this.userName = UserName;
84 this.password = Password;
85 }
86
90 public async Task Connect()
91 {
92 if (!(this.client is null))
93 {
94 await this.client.DisposeAsync();
95 this.client = null;
96 }
97
98 lock (this.synchObj)
99 {
100 this.response.Clear();
101 }
102
103 this.client = new RowTcpClient(Encoding.UTF8, 10000, false);
104 this.client.Client.ReceiveTimeout = 10000;
105 this.client.Client.SendTimeout = 10000;
106
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;
112
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());
116
117 await this.AssertOkResult();
118 }
119
120 private string Client_OnWarning(string Text)
121 {
122 this.Warning(Text);
123 return Text;
124 }
125
126 private string Client_OnInformation(string Text)
127 {
128 this.Information(Text);
129 return Text;
130 }
131
132 private Task Client_OnError(object Sender, Exception Exception)
133 {
134 this.Error(Exception.Message);
135 return Task.CompletedTask;
136 }
137
138 private Task<bool> Client_OnSent(object Sender, string Text)
139 {
140 this.TransmitText(Text);
141 return Task.FromResult(true);
142 }
143
144 private Task<bool> Client_OnReceived(object Sender, string Row)
145 {
146 if (string.IsNullOrEmpty(Row))
147 {
148 this.Error("No response returned.");
149 return Task.FromResult(true);
150 }
151
152 this.ReceiveText(Row);
153
154 int i = Row.IndexOfAny(spaceHyphen);
155 if (i < 0)
156 i = Row.Length;
157
158 if (!int.TryParse(Row[..i], out int Code))
159 {
160 this.Error("Invalid response returned.");
161 return Task.FromResult(true);
162 }
163
164 bool More = i < Row.Length && Row[i] == '-';
165
166 lock (this.synchObj)
167 {
168 if (i < Row.Length)
169 Row = Row[(i + 1)..].Trim();
170 else
171 Row = string.Empty;
172
173 this.response.Add(new KeyValuePair<int, string>(Code, Row));
174
175 if (!More)
176 {
177 this.responseSource.TrySetResult(this.response.ToArray());
178 this.response.Clear();
179 }
180 }
181
182 return Task.FromResult(true);
183 }
184
188 [Obsolete("Use DisposeAsync() instead.")]
189 public void Dispose()
190 {
191 this.DisposeAsync().Wait();
192 }
193
197 public async Task DisposeAsync()
198 {
199 if (!(this.client is null))
200 {
201 await this.client.DisposeAsync();
202 this.client = null;
203 }
204 }
205
209 public string Domain => this.domain;
210
215 {
216 get => this.trustCertificate;
217 set => this.trustCertificate = value;
218 }
219
223 public X509Certificate ServerCertificate => this.client.RemoteCertificate;
224
228 public bool ServerCertificateValid => this.client.RemoteCertificateValid;
229
234 {
235 get => this.permittedAuthenticationMechanisms;
236 set => this.permittedAuthenticationMechanisms = value;
237 }
238
243 public Task<KeyValuePair<int, string>[]> ReadResponse()
244 {
245 return this.ReadResponse(10000);
246 }
247
253 public async Task<KeyValuePair<int, string>[]> ReadResponse(int Timeout)
254 {
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.");
258
259 return Source.Task.Result;
260 }
261
262 private static readonly char[] spaceHyphen = new char[] { ' ', '-' };
263
264 private Task WriteLine(string Row)
265 {
266 lock (this.synchObj)
267 {
268 this.response.Clear();
269 this.responseSource = new TaskCompletionSource<KeyValuePair<int, string>[]>();
270 }
271
272 return this.client.SendAsync(Row);
273 }
274
275 private Task Write(bool ConstantBuffer, byte[] Bytes)
276 {
277 return this.client.SendAsync(ConstantBuffer, Bytes);
278 }
279
280 private Task<string> AssertOkResult()
281 {
282 return this.AssertResult(300);
283 }
284
285 private Task<string> AssertContinue()
286 {
287 return this.AssertResult(400);
288 }
289
290 private async Task<string> AssertResult(int MaxExclusive)
291 {
292 KeyValuePair<int, string>[] Response = await this.ReadResponse();
293 int Code = Response[0].Key;
294 string Message = Response[0].Value.Trim();
295
296 if (string.IsNullOrEmpty(Message))
297 Message = "Request rejected.";
298
299 if (Code < 200 || Code >= MaxExclusive)
300 {
301 if (Code >= 400 && Code < 500)
302 throw new SmtpTemporaryErrorException(Message, Code);
303 else
304 throw new SmtpException(Message, Code);
305 }
306
307 return Response[0].Value;
308 }
309
317 public async Task<string> EHLO(string Domain)
318 {
319 this.startTls = false;
320 //this.size = null;
321 this.authMechanisms = null;
322 //this.smptUtf8 = false;
323 //this.eightBitMime = false;
324 //this.enhancedStatusCodes = false;
325 //this.help = false;
326
327 if (string.IsNullOrEmpty(Domain))
328 await this.WriteLine("EHLO");
329 else
330 await this.WriteLine("EHLO " + Domain);
331
332 KeyValuePair<int, string>[] Response = await this.ReadResponse();
333 if (Response[0].Key < 200 || Response[0].Key >= 300)
334 throw new IOException("Request rejected.");
335
336 int i = Response[0].Value.LastIndexOf('[');
337 int j = Response[0].Value.LastIndexOf(']');
338 string ResponseDomain;
339
340 if (i >= 0 && j > i)
341 {
342 ResponseDomain = Response[0].Value.Substring(i + 1, j - i - 1);
343
344 if (string.IsNullOrEmpty(Domain))
345 Domain = ResponseDomain;
346
347 if (string.IsNullOrEmpty(this.domain))
348 this.domain = ResponseDomain;
349 }
350 else
351 ResponseDomain = string.Empty;
352
353 foreach (KeyValuePair<int, string> P in Response)
354 {
355 string s = P.Value.ToUpper();
356
357 switch (s)
358 {
359 case "STARTTLS":
360 this.startTls = true;
361 break;
362
363 case "SMTPUTF8":
364 //this.smptUtf8 = true;
365 break;
366
367 case "8BITMIME":
368 //this.eightBitMime = true;
369 break;
370
371 case "ENHANCEDSTATUSCODES":
372 //this.enhancedStatusCodes = true;
373 break;
374
375 case "HELP":
376 //this.help = true;
377 break;
378
379 default:
380 /*if (s.StartsWith("SIZE "))
381 {
382 if (int.TryParse(s.Substring(5).Trim(), out int i))
383 this.size = i;
384 }
385 else*/
386 if (s.StartsWith("AUTH "))
387 this.authMechanisms = s[5..].Trim().Split(space, StringSplitOptions.RemoveEmptyEntries);
388 break;
389 }
390 }
391
392 if (this.startTls && !(this.client.Stream is SslStream))
393 {
394 await this.WriteLine("STARTTLS");
395 await this.AssertOkResult(); // Will pause when complete.
396
397 await this.client.PauseReading();
398
399 this.Information("Starting TLS handshake.");
400 await this.client.UpgradeToTlsAsClient(null, Crypto.SecureTls, this.trustCertificate);
401 this.Information("TLS handshake complete.");
402 this.client.Continue();
403
404 ResponseDomain = await this.EHLO(Domain);
405 }
406 else if (!(this.authMechanisms is null) && !string.IsNullOrEmpty(this.userName) && !string.IsNullOrEmpty(this.password))
407 {
408 foreach (string Mechanism in this.authMechanisms)
409 {
410 if (Mechanism == "EXTERNAL")
411 continue;
412
413 SslStream SslStream = this.client.Stream as SslStream;
415 {
416 if (M.Name != Mechanism)
417 continue;
418
419 if (!M.Allowed(SslStream))
420 break;
421
422 if (!(this.permittedAuthenticationMechanisms is null) &&
423 Array.IndexOf(this.permittedAuthenticationMechanisms, Mechanism) < 0)
424 {
425 break;
426 }
427
428 bool? b;
429
430 try
431 {
432 b = await M.Authenticate(this.userName, this.password, this);
433 if (!b.HasValue)
434 continue;
435 }
436 catch (Exception)
437 {
438 b = false;
439 }
440
441 if (!b.Value)
442 throw new AuthenticationException("Unable to authenticate user.");
443
444 return ResponseDomain;
445 }
446 }
447
448 throw new AuthenticationException("No suitable and supported authentication mechanism found.");
449 }
450
451 return ResponseDomain;
452 }
453
454 private static readonly char[] space = new char[] { ' ' };
455
460 public async Task VRFY(string Account)
461 {
462 await this.WriteLine("VRFY " + Account);
463 await this.AssertOkResult();
464 }
465
470 public async Task MAIL_FROM(string Sender)
471 {
472 await this.WriteLine("MAIL FROM: <" + Sender + ">");
473 await this.AssertOkResult();
474 }
475
480 public async Task RCPT_TO(string Receiver)
481 {
482 await this.WriteLine("RCPT TO: <" + Receiver + ">");
483 await this.AssertOkResult();
484 }
485
489 public async Task QUIT()
490 {
491 await this.WriteLine("QUIT");
492 await this.AssertOkResult();
493 }
494
498 public Task DATA(KeyValuePair<string, string>[] Headers, byte[] Body)
499 {
500 return this.DATA(Headers, false, Body);
501 }
502
506 public async Task DATA(KeyValuePair<string, string>[] Headers, bool ConstantBody, byte[] Body)
507 {
508 await this.WriteLine("DATA");
509 await this.AssertContinue();
510
511 foreach (KeyValuePair<string, string> Header in Headers)
512 await this.WriteLine(Header.Key + ": " + Header.Value);
513
514 await this.WriteLine(string.Empty);
515
516 int c = Body.Length;
517 int i = 0;
518 int j;
519
520 while (i < c)
521 {
522 j = this.IndexOf(Body, crLfDot, i);
523 if (j < 0)
524 j = c;
525
526 if (i == 0 && j == c)
527 await this.Write(ConstantBody, Body);
528 else
529 {
530 byte[] Bin = new byte[j - i];
531 Buffer.BlockCopy(Body, i, Bin, 0, j - i);
532 await this.Write(true, Bin);
533 }
534
535 i = j;
536 if (i < c)
537 {
538 await this.Write(true, crLfDot);
539 i += 2;
540 }
541 }
542
543 await this.WriteLine(string.Empty);
544 await this.WriteLine(string.Empty);
545 await this.WriteLine(".");
546
547 await this.AssertOkResult();
548 }
549
550 private static readonly byte[] crLfDot = new byte[] { (byte)'\r', (byte)'\n', (byte)'.' };
551
552 private int IndexOf(byte[] Data, byte[] Segment, int StartIndex)
553 {
554 int i, j;
555 int d = Segment.Length;
556 int c = Data.Length - d + 1;
557
558 for (i = StartIndex; i < c; i++)
559 {
560 for (j = 0; j < d; j++)
561 {
562 if (Data[i + j] != Segment[j])
563 break;
564 }
565
566 if (j == d)
567 return i;
568 }
569
570 return -1;
571 }
572
579 public async Task<string> Initiate(IAuthenticationMechanism Mechanism, string Parameters)
580 {
581 string s = "AUTH " + Mechanism.Name;
582 if (!string.IsNullOrEmpty(Parameters))
583 s += " " + Parameters;
584
585 await this.WriteLine(s);
586 return await this.AssertContinue();
587 }
588
595 public async Task<string> ChallengeResponse(IAuthenticationMechanism Mechanism, string Parameters)
596 {
597 await this.WriteLine(Parameters);
598 return await this.AssertContinue();
599 }
600
607 public async Task<string> FinalResponse(IAuthenticationMechanism Mechanism, string Parameters)
608 {
609 await this.WriteLine(Parameters);
610 await this.AssertOkResult();
611
612 return null; // No response in SMTP
613 }
614
623 public async Task SendFormattedEMail(string Sender, string Recipient, string Subject,
624 string MarkdownContent, params object[] Attachments)
625 {
627 string HTML = "<html><body>" + HtmlDocument.GetBody(await Doc.GenerateHTML()) + "</body></html>";
628 string PlainText = await Doc.GeneratePlainText();
630 {
631 new EmbeddedContent()
632 {
633 ContentType = "text/html; charset=utf-8",
634 Raw = Encoding.UTF8.GetBytes(HTML)
635 },
636 new EmbeddedContent()
637 {
638 ContentType = PlainTextCodec.DefaultContentType + "; charset=utf-8",
639 Raw = Encoding.UTF8.GetBytes(PlainText)
640 },
641 new EmbeddedContent()
642 {
643 ContentType = "text/markdown; charset=utf-8",
644 Raw = Encoding.UTF8.GetBytes(MarkdownContent)
645 }
646 });
647
648 ContentResponse P = await InternetContent.EncodeAsync(Content, Encoding.UTF8);
649
650 if (Attachments.Length > 0)
651 {
652 List<EmbeddedContent> Parts = new List<EmbeddedContent>()
653 {
654 new EmbeddedContent()
655 {
656 ContentType = P.ContentType,
657 Raw = P.Encoded
658 }
659 };
660
661 foreach (object Attachment in Attachments)
662 Parts.Add(await EmbeddedContent.Encode(Attachment, Encoding.UTF8));
663
664 MixedContent Mixed = new MixedContent(Parts.ToArray());
665
666 P = await InternetContent.EncodeAsync(Mixed, Encoding.UTF8);
667 }
668
669 byte[] BodyBin = P.Encoded;
670 string ContentType = P.ContentType;
671
672 List<KeyValuePair<string, string>> Headers = new List<KeyValuePair<string, string>>()
673 {
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),
678 new KeyValuePair<string, string>("DATE", CommonTypes.EncodeRfc822(DateTime.Now)),
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)
683 };
684
685 await this.MAIL_FROM(Sender);
686 await this.RCPT_TO(Recipient);
687 await this.DATA(Headers.ToArray(), true, BodyBin);
688 }
689
690 }
691}
Helps with parsing of commong data types.
Definition: CommonTypes.cs:15
static string EncodeRfc822(DateTime Timestamp)
Encodes a date and time, according to RFC 822 §5.
Definition: CommonTypes.cs:669
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
Definition: MixedContent.cs:7
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....
Definition: RowTcpClient.cs:18
Module maintaining available SASL mechanisms.
Definition: SaslModule.cs:14
static IAuthenticationMechanism[] Mechanisms
Available SASL mechanisms.
Definition: SaslModule.cs:39
Base class for SMTP-related exceptions.
Definition: SmtpException.cs:9
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).
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
async Task< KeyValuePair< int, string >[]> ReadResponse(int Timeout)
Reads a response from the server.
Helper methods for encrypting and decrypting streams of data.
Definition: Crypto.cs:14
const SslProtocols SecureTls
TLS 1.2 & 1.3
Definition: Crypto.cs:18
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.
Interface for client-side client connections.
Interface for sniffers. Sniffers can be added to ICommunicationLayer classes to eavesdrop on communic...
Definition: ISniffer.cs:10