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;
2using System.Collections.Generic;
3using System.IO;
4using System.Net.Security;
5using System.Security.Authentication;
6using System.Security.Cryptography.X509Certificates;
7using System.Text;
8using System.Threading.Tasks;
9using Waher.Content;
17
19{
24 {
28 public const int DefaultSmtpPort = 25;
29
33 public const int AlternativeSmtpPort = 587;
34
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>[]>();
37 private RowTcpClient client;
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;
45 //private bool smptUtf8 = false;
46 //private bool eightBitMime = false;
47 //private bool enhancedStatusCodes = false;
48 //private bool help = false;
49 private bool trustCertificate = false;
50 //private int? size = null;
51 private string[] authMechanisms = null;
52 private string[] permittedAuthenticationMechanisms = null;
53
61 public SimpleSmtpClient(string Domain, string Host, int Port, params ISniffer[] Sniffers)
62 : this(Domain, Host, Port, null, null, Sniffers)
63 {
64 }
65
75 public SimpleSmtpClient(string Domain, string Host, int Port, string UserName, string Password, params ISniffer[] Sniffers)
76 : base(false, Sniffers)
77 {
78 this.domain = Domain;
79 this.host = Host;
80 this.port = Port;
81 this.userName = UserName;
82 this.password = Password;
83 }
84
88 public async Task Connect()
89 {
90 this.client?.Dispose();
91 this.client = null;
92
93 lock (this.synchObj)
94 {
95 this.response.Clear();
96 }
97
98 this.client = new RowTcpClient(Encoding.UTF8, 10000, false);
99 this.client.Client.ReceiveTimeout = 10000;
100 this.client.Client.SendTimeout = 10000;
101
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;
107
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());
111
112 await this.AssertOkResult();
113 }
114
115 private async Task<string> Client_OnWarning(string Text)
116 {
117 await this.Warning(Text);
118 return Text;
119 }
120
121 private async Task<string> Client_OnInformation(string Text)
122 {
123 await this.Information(Text);
124 return Text;
125 }
126
127 private Task Client_OnError(object Sender, Exception Exception)
128 {
129 return this.Error(Exception.Message);
130 }
131
132 private async Task<bool> Client_OnSent(object Sender, string Text)
133 {
134 await this.TransmitText(Text);
135 return true;
136 }
137
138 private async Task<bool> Client_OnReceived(object Sender, string Row)
139 {
140 if (string.IsNullOrEmpty(Row))
141 {
142 await this.Error("No response returned.");
143 return true;
144 }
145
146 await this.ReceiveText(Row);
147
148 int i = Row.IndexOfAny(spaceHyphen);
149 if (i < 0)
150 i = Row.Length;
151
152 if (!int.TryParse(Row.Substring(0, i), out int Code))
153 {
154 await this.Error("Invalid response returned.");
155 return true;
156 }
157
158 bool More = i < Row.Length && Row[i] == '-';
159
160 lock (this.synchObj)
161 {
162 if (i < Row.Length)
163 Row = Row.Substring(i + 1).Trim();
164 else
165 Row = string.Empty;
166
167 this.response.Add(new KeyValuePair<int, string>(Code, Row));
168
169 if (!More)
170 {
171 this.responseSource.TrySetResult(this.response.ToArray());
172 this.response.Clear();
173 }
174 }
175
176 return true;
177 }
178
182 public void Dispose()
183 {
184 this.client?.Dispose();
185 this.client = null;
186 }
187
191 public string Domain
192 {
193 get => this.domain;
194 }
195
200 {
201 get => this.trustCertificate;
202 set => this.trustCertificate = value;
203 }
204
208 public X509Certificate ServerCertificate => this.client.RemoteCertificate;
209
213 public bool ServerCertificateValid => this.client.RemoteCertificateValid;
214
219 {
220 get => this.permittedAuthenticationMechanisms;
221 set => this.permittedAuthenticationMechanisms = value;
222 }
223
228 public Task<KeyValuePair<int, string>[]> ReadResponse()
229 {
230 return this.ReadResponse(10000);
231 }
232
238 public async Task<KeyValuePair<int, string>[]> ReadResponse(int Timeout)
239 {
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.");
243
244 return Source.Task.Result;
245 }
246
247 private static readonly char[] spaceHyphen = new char[] { ' ', '-' };
248
249 private Task WriteLine(string Row)
250 {
251 lock (this.synchObj)
252 {
253 this.response.Clear();
254 this.responseSource = new TaskCompletionSource<KeyValuePair<int, string>[]>();
255 }
256
257 return this.client.SendAsync(Row);
258 }
259
260 private Task Write(byte[] Bytes)
261 {
262 return this.client.SendAsync(Bytes);
263 }
264
265 private Task<string> AssertOkResult()
266 {
267 return this.AssertResult(300);
268 }
269
270 private Task<string> AssertContinue()
271 {
272 return this.AssertResult(400);
273 }
274
275 private async Task<string> AssertResult(int MaxExclusive)
276 {
277 KeyValuePair<int, string>[] Response = await this.ReadResponse();
278 int Code = Response[0].Key;
279 string Message = Response[0].Value.Trim();
280
281 if (string.IsNullOrEmpty(Message))
282 Message = "Request rejected.";
283
284 if (Code < 200 || Code >= MaxExclusive)
285 {
286 if (Code >= 400 && Code < 500)
287 throw new SmtpTemporaryErrorException(Message, Code);
288 else
289 throw new SmtpException(Message, Code);
290 }
291
292 return Response[0].Value;
293 }
294
302 public async Task<string> EHLO(string Domain)
303 {
304 this.startTls = false;
305 //this.size = null;
306 this.authMechanisms = null;
307 //this.smptUtf8 = false;
308 //this.eightBitMime = false;
309 //this.enhancedStatusCodes = false;
310 //this.help = false;
311
312 if (string.IsNullOrEmpty(Domain))
313 await this.WriteLine("EHLO");
314 else
315 await this.WriteLine("EHLO " + Domain);
316
317 KeyValuePair<int, string>[] Response = await this.ReadResponse();
318 if (Response[0].Key < 200 || Response[0].Key >= 300)
319 throw new IOException("Request rejected.");
320
321 int i = Response[0].Value.LastIndexOf('[');
322 int j = Response[0].Value.LastIndexOf(']');
323 string ResponseDomain;
324
325 if (i >= 0 && j > i)
326 {
327 ResponseDomain = Response[0].Value.Substring(i + 1, j - i - 1);
328
329 if (string.IsNullOrEmpty(Domain))
330 Domain = ResponseDomain;
331
332 if (string.IsNullOrEmpty(this.domain))
333 this.domain = ResponseDomain;
334 }
335 else
336 ResponseDomain = string.Empty;
337
338 foreach (KeyValuePair<int, string> P in Response)
339 {
340 string s = P.Value.ToUpper();
341
342 switch (s)
343 {
344 case "STARTTLS":
345 this.startTls = true;
346 break;
347
348 case "SMTPUTF8":
349 //this.smptUtf8 = true;
350 break;
351
352 case "8BITMIME":
353 //this.eightBitMime = true;
354 break;
355
356 case "ENHANCEDSTATUSCODES":
357 //this.enhancedStatusCodes = true;
358 break;
359
360 case "HELP":
361 //this.help = true;
362 break;
363
364 default:
365 /*if (s.StartsWith("SIZE "))
366 {
367 if (int.TryParse(s.Substring(5).Trim(), out int i))
368 this.size = i;
369 }
370 else*/
371 if (s.StartsWith("AUTH "))
372 this.authMechanisms = s.Substring(5).Trim().Split(space, StringSplitOptions.RemoveEmptyEntries);
373 break;
374 }
375 }
376
377 if (this.startTls && !(this.client.Stream is SslStream))
378 {
379 await this.WriteLine("STARTTLS");
380 await this.AssertOkResult(); // Will pause when complete.
381
382 await this.client.PauseReading();
383
384 await this.Information("Starting TLS handshake.");
385 await this.client.UpgradeToTlsAsClient(null, SslProtocols.Tls12, this.trustCertificate);
386 await this.Information("TLS handshake complete.");
387 this.client.Continue();
388
389 ResponseDomain = await this.EHLO(Domain);
390 }
391 else if (!(this.authMechanisms is null) && !string.IsNullOrEmpty(this.userName) && !string.IsNullOrEmpty(this.password))
392 {
393 foreach (string Mechanism in this.authMechanisms)
394 {
395 if (Mechanism == "EXTERNAL")
396 continue;
397
398 SslStream SslStream = this.client.Stream as SslStream;
400 {
401 if (M.Name != Mechanism)
402 continue;
403
404 if (!M.Allowed(SslStream))
405 break;
406
407 if (!(this.permittedAuthenticationMechanisms is null) &&
408 Array.IndexOf(this.permittedAuthenticationMechanisms, Mechanism) < 0)
409 {
410 break;
411 }
412
413 bool? b;
414
415 try
416 {
417 b = await M.Authenticate(this.userName, this.password, this);
418 if (!b.HasValue)
419 continue;
420 }
421 catch (Exception)
422 {
423 b = false;
424 }
425
426 if (!b.Value)
427 throw new AuthenticationException("Unable to authenticate user.");
428
429 return ResponseDomain;
430 }
431 }
432
433 throw new AuthenticationException("No suitable and supported authentication mechanism found.");
434 }
435
436 return ResponseDomain;
437 }
438
439 private static readonly char[] space = new char[] { ' ' };
440
445 public async Task VRFY(string Account)
446 {
447 await this.WriteLine("VRFY " + Account);
448 await this.AssertOkResult();
449 }
450
455 public async Task MAIL_FROM(string Sender)
456 {
457 await this.WriteLine("MAIL FROM: <" + Sender + ">");
458 await this.AssertOkResult();
459 }
460
465 public async Task RCPT_TO(string Receiver)
466 {
467 await this.WriteLine("RCPT TO: <" + Receiver + ">");
468 await this.AssertOkResult();
469 }
470
474 public async Task QUIT()
475 {
476 await this.WriteLine("QUIT");
477 await this.AssertOkResult();
478 }
479
483 public async Task DATA(KeyValuePair<string, string>[] Headers, byte[] Body)
484 {
485 await this.WriteLine("DATA");
486 await this.AssertContinue();
487
488 foreach (KeyValuePair<string, string> Header in Headers)
489 await this.WriteLine(Header.Key + ": " + Header.Value);
490
491 await this.WriteLine(string.Empty);
492
493 int c = Body.Length;
494 int i = 0;
495 int j;
496
497 while (i < c)
498 {
499 j = this.IndexOf(Body, crLfDot, i);
500 if (j < 0)
501 j = c;
502
503 if (i == 0 && j == c)
504 await this.Write(Body);
505 else
506 {
507 byte[] Bin = new byte[j - i];
508 Array.Copy(Body, i, Bin, 0, j - i);
509 await this.Write(Bin);
510 }
511
512 i = j;
513 if (i < c)
514 {
515 await this.Write(crLfDot);
516 i += 2;
517 }
518 }
519
520 await this.WriteLine(string.Empty);
521 await this.WriteLine(string.Empty);
522 await this.WriteLine(".");
523
524 await this.AssertOkResult();
525 }
526
527 private static readonly byte[] crLfDot = new byte[] { (byte)'\r', (byte)'\n', (byte)'.' };
528
529 private int IndexOf(byte[] Data, byte[] Segment, int StartIndex)
530 {
531 int i, j;
532 int d = Segment.Length;
533 int c = Data.Length - d + 1;
534
535 for (i = StartIndex; i < c; i++)
536 {
537 for (j = 0; j < d; j++)
538 {
539 if (Data[i + j] != Segment[j])
540 break;
541 }
542
543 if (j == d)
544 return i;
545 }
546
547 return -1;
548 }
549
556 public async Task<string> Initiate(IAuthenticationMechanism Mechanism, string Parameters)
557 {
558 string s = "AUTH " + Mechanism.Name;
559 if (!string.IsNullOrEmpty(Parameters))
560 s += " " + Parameters;
561
562 await this.WriteLine(s);
563 return await this.AssertContinue();
564 }
565
572 public async Task<string> ChallengeResponse(IAuthenticationMechanism Mechanism, string Parameters)
573 {
574 await this.WriteLine(Parameters);
575 return await this.AssertContinue();
576 }
577
584 public async Task<string> FinalResponse(IAuthenticationMechanism Mechanism, string Parameters)
585 {
586 await this.WriteLine(Parameters);
587 await this.AssertOkResult();
588
589 return null; // No response in SMTP
590 }
591
600 public async Task SendFormattedEMail(string Sender, string Recipient, string Subject,
601 string MarkdownContent, params object[] Attachments)
602 {
604 string HTML = "<html><body>" + HtmlDocument.GetBody(await Doc.GenerateHTML()) + "</body></html>";
605 string PlainText = await Doc.GeneratePlainText();
607 {
608 new EmbeddedContent()
609 {
610 ContentType = "text/html; charset=utf-8",
611 Raw = Encoding.UTF8.GetBytes(HTML)
612 },
613 new EmbeddedContent()
614 {
615 ContentType = PlainTextCodec.DefaultContentType + "; charset=utf-8",
616 Raw = Encoding.UTF8.GetBytes(PlainText)
617 },
618 new EmbeddedContent()
619 {
620 ContentType = "text/markdown; charset=utf-8",
621 Raw = Encoding.UTF8.GetBytes(MarkdownContent)
622 }
623 });
624
625 KeyValuePair<byte[], string> P = await InternetContent.EncodeAsync(Content, Encoding.UTF8);
626
627 if (Attachments.Length > 0)
628 {
629 List<EmbeddedContent> Parts = new List<EmbeddedContent>()
630 {
631 new EmbeddedContent()
632 {
633 ContentType = P.Value,
634 Raw = P.Key
635 }
636 };
637
638 foreach (object Attachment in Attachments)
639 {
640 KeyValuePair<byte[], string> P2 = await InternetContent.EncodeAsync(Attachment, Encoding.UTF8);
641 Parts.Add(new EmbeddedContent()
642 {
643 ContentType = P2.Value,
644 Raw = P2.Key
645 });
646 }
647
648 MixedContent Mixed = new MixedContent(Parts.ToArray());
649
650 P = await InternetContent.EncodeAsync(Mixed, Encoding.UTF8);
651 }
652
653 byte[] BodyBin = P.Key;
654 string ContentType = P.Value;
655
656 List<KeyValuePair<string, string>> Headers = new List<KeyValuePair<string, string>>()
657 {
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),
662 new KeyValuePair<string, string>("DATE", CommonTypes.EncodeRfc822(DateTime.Now)),
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)
667 };
668
669 await this.MAIL_FROM(Sender);
670 await this.RCPT_TO(Recipient);
671 await this.DATA(Headers.ToArray(), BodyBin);
672 }
673
674 }
675}
Helps with parsing of commong data types.
Definition: CommonTypes.cs:13
static string EncodeRfc822(DateTime Timestamp)
Encodes a date and time, according to RFC 822 §5.
Definition: CommonTypes.cs:667
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
Definition: MixedContent.cs:7
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....
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
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.
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
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.
Interface for client-side client connections.
Interface for sniffers. Sniffers can be added to ICommunicationLayer classes to eavesdrop on communic...
Definition: ISniffer.cs:11