2using System.Collections.Generic;
4using System.Net.NetworkInformation;
5using System.Net.Sockets;
6using System.Security.Cryptography.X509Certificates;
8using System.Text.RegularExpressions;
9using System.Threading.Tasks;
70 private static string salutation =
null;
71 private static DateTime salutationExpires = DateTime.MinValue;
73 private LinkedList<TcpListener> listeners =
new LinkedList<TcpListener>();
76 private X509Certificate serverCertificate;
79 private readonly
bool encryptionRequired;
80 private readonly
int maxMessageSize;
81 private readonly
string[] ip4DnsBlackLists;
82 private readonly
string[] ip6DnsBlackLists;
84 private string smtpSnifferPath =
null;
85 private bool disposed =
false;
88 private string[] relayDomains =
null;
89 private Regex[] relayDomainsEx =
null;
90 private string relayHost =
string.Empty;
91 private string relayUserName =
string.Empty;
92 private string relayPassword =
string.Empty;
93 private int relayPort = 587;
94 private bool useRelayServer =
false;
95 private bool relayLocked =
false;
112 string[] Ip4DnsBlackLists,
string[] Ip6DnsBlackLists,
SpfExpression[] SpfExpressions)
114 EncryptionRequired, PersistenceLayer, Ip4DnsBlackLists, Ip6DnsBlackLists, SpfExpressions)
132 string[] Ip4DnsBlackLists,
string[] Ip6DnsBlackLists,
SpfExpression[] SpfExpressions)
134 Ip4DnsBlackLists, Ip6DnsBlackLists, SpfExpressions)
154 this.persistenceLayer = PersistenceLayer;
158 this.maxMessageSize = MaxMessageSize;
159 this.ip4DnsBlackLists = Ip4DnsBlackLists;
160 this.ip6DnsBlackLists = Ip6DnsBlackLists;
161 this.spfExpressions = SpfExpressions;
164 throw new ArgumentException(
"Server Certificate must be provided, if encryption is required.", nameof(
ServerCertificate));
167 this.clientConnections.Removed += this.ClientConnections_Removed;
169 this.Initialize(Ports);
172 private async
void Initialize(
int[] Ports)
176 TcpListener Listener;
178 foreach (NetworkInterface Interface
in NetworkInterface.GetAllNetworkInterfaces())
180 if (Interface.OperationalStatus != OperationalStatus.Up)
183 IPInterfaceProperties Properties = Interface.GetIPProperties();
185 foreach (UnicastIPAddressInformation UnicastAddress
in Properties.UnicastAddresses)
187 if ((UnicastAddress.Address.AddressFamily == AddressFamily.InterNetwork && Socket.OSSupportsIPv4) ||
188 (UnicastAddress.Address.AddressFamily == AddressFamily.InterNetworkV6 && Socket.OSSupportsIPv6))
190 if (!(Ports is
null))
192 foreach (
int Port
in Ports)
196 await this.externalSniffers.Information(
"Opening port " + Port.ToString() +
" on " + UnicastAddress.Address.ToString() +
".");
198 Listener =
new TcpListener(UnicastAddress.Address, Port);
200 Listener.BeginAcceptTcpClient(this.AcceptTcpClientCallback, Listener);
201 this.listeners.AddLast(Listener);
203 await this.externalSniffers.Information(
"Port " + Port.ToString() +
" on " + UnicastAddress.Address.ToString() +
" opened.");
207 Log.
Exception(ex, UnicastAddress.Address.ToString() +
":" + Port);
232 get => this.smtpSnifferPath;
233 set => this.smtpSnifferPath = value;
241 await e.
Value.DisposeAsync();
254 get => this.clientConnections.
Count;
259 List<SmtpClientConnection> Connections =
new List<SmtpClientConnection>();
261 foreach (Guid Id
in this.clientConnections.
GetKeys())
264 Connections.Add(Connection);
267 Connections.Sort((c1, c2) =>
269 return c1.UserName.CompareTo(c2.UserName);
272 return Connections.ToArray();
275 public bool TryGetClientConnection(Guid ID, out SmtpClientConnection Connection)
277 return this.clientConnections.
TryGetValue(ID, out Connection);
293 get => this.serverCertificate;
310 get => this.encryptionRequired;
315 get => this.persistenceLayer;
318 internal string[] Ip4DnsBlackLists => this.ip4DnsBlackLists;
319 internal string[] Ip6DnsBlackLists => this.ip6DnsBlackLists;
320 internal SpfExpression[] SpfExpressions => this.spfExpressions;
327 this.disposed =
true;
329 if (!(this.clientConnections is
null))
331 this.clientConnections.
Clear();
332 this.clientConnections.
Dispose();
333 this.clientConnections =
null;
336 if (!(this.listeners is
null))
338 LinkedList<TcpListener> Listeners = this.listeners;
339 this.listeners =
null;
341 foreach (TcpListener Listener
in Listeners)
345 if (!(this.sniffers is
null))
347 this.sniffers.
Clear();
349 this.sniffers =
null;
352 if (!(this.externalSniffers is
null))
354 foreach (
ISniffer Sniffer
in this.externalSniffers)
355 (Sniffer as IDisposable)?.
Dispose();
366 return this.GetOpenPorts(this.listeners);
370 private int[] GetOpenPorts(LinkedList<TcpListener> Listeners)
372 SortedDictionary<int, bool> Open =
new SortedDictionary<int, bool>();
374 if (!(Listeners is
null))
376 IPEndPoint IPEndPoint;
378 foreach (TcpListener Listener
in Listeners)
380 IPEndPoint = Listener.LocalEndpoint as IPEndPoint;
381 if (!(IPEndPoint is
null))
382 Open[IPEndPoint.Port] =
true;
386 int[] Result =
new int[Open.Count];
387 Open.Keys.CopyTo(Result, 0);
396 private async
void AcceptTcpClientCallback(IAsyncResult ar)
403 TcpListener Listener = (TcpListener)ar.AsyncState;
407 TcpClient Client = Listener.EndAcceptTcpClient(ar);
408 SmtpClientConnection ClientConnection;
411 if (!
string.IsNullOrEmpty(this.smtpSnifferPath))
413 else if (this.externalSniffers.HasSniffers)
414 Sniffers = this.externalSniffers.Sniffers;
421 ClientConnection =
new SmtpClientConnection(
BinaryTcpClient,
this, this.persistenceLayer, this.maxMessageSize, Sniffers);
422 await ClientConnection.Information(
"Connection accepted from " + Client.Client.RemoteEndPoint.ToString() +
".");
424 this.clientConnections[ClientConnection.ID] = ClientConnection;
429 await ClientConnection.BeginWrite(
"220 " + this.domain +
" ESMTP Sendmail ...\r\n",
null,
null);
434 Listener.BeginAcceptTcpClient(this.AcceptTcpClientCallback, Listener);
437 catch (SocketException)
441 catch (ObjectDisposedException)
445 catch (NullReferenceException)
451 if (this.listeners is
null)
458 internal string GetTransformPath()
460 foreach (
ISniffer Sniffer
in this.externalSniffers.Sniffers)
469 internal void Closed(SmtpClientConnection Connection)
471 this.clientConnections?.
Remove(Connection.ID);
485 return this.persistenceLayer.GetAccount(UserName);
491 public event EventHandlerAsync<ClientConnectionEventArgs> ClientConnectionAdded =
null;
496 public event EventHandlerAsync<ClientConnectionEventArgs> ClientConnectionRemoved =
null;
505 public event EventHandlerAsync<SmtpMessageEventArgs> MessageReceived =
null;
520 FileName = this.smtpSnifferPath.Replace(
"%ENDPOINT%", Key);
528 internal void CacheSniffers(IEnumerable<ISniffer> Sniffers)
530 foreach (
ISniffer Sniffer
in Sniffers)
534 else if (Sniffer is IDisposable Disposable)
535 Disposable.Dispose();
541 if (this.sniffers is
null)
544 this.sniffers.Removed += this.Sniffers_Removed;
552 if (this.disposed || (DateTime.Now - e.
Value.LastEvent).TotalMinutes > 30)
555 return Task.CompletedTask;
573 string UserName,
string Password,
string[] RelayDomains,
bool LockSettings)
575 if (this.useRelayServer != UseRelayServer ||
576 this.relayHost != HostName ||
577 this.relayPort != PortNumber ||
578 this.relayUserName != UserName ||
579 this.relayPassword != Password ||
580 !AreSame(this.relayDomains, RelayDomains))
582 if (this.relayLocked)
583 throw new InvalidOperationException(
"Relay settings locked.");
585 this.useRelayServer = UseRelayServer;
586 this.relayHost = HostName;
587 this.relayPort = PortNumber;
588 this.relayUserName = UserName;
589 this.relayPassword = Password;
590 this.relayDomains = RelayDomains;
591 this.relayDomainsEx =
null;
595 this.relayLocked =
true;
598 private static bool AreSame(
string[] A1,
string[] A2)
600 if ((A1 is
null) ^ (A2 is
null))
612 for (i = 0; i < c; i++)
628 if (this.relayDomains is
null)
631 int i, c = this.relayDomains.Length;
633 for (i = 0; i < c; i++)
635 string s = this.relayDomains[i];
637 if (
string.Compare(s, Domain,
true) == 0)
640 if (s.IndexOf(
'*') < 0)
643 if (this.relayDomainsEx is
null)
644 this.relayDomainsEx =
new Regex[this.relayDomains.Length];
646 if (this.relayDomainsEx[i] is
null)
647 this.relayDomainsEx[i] =
new Regex(
Database.
WildcardToRegex(s,
"*"), RegexOptions.Singleline | RegexOptions.IgnoreCase);
649 Match M = this.relayDomainsEx[i].Match(Domain);
650 if (M.Success && M.Index == 0 && M.Length == Domain.Length)
665 string Subject,
object[] AlternativeBodies)
667 int i, c = AlternativeBodies.Length;
670 for (i = 0; i < c; i++)
673 byte[] EncodedBody = P.Key;
674 string BodyContentType = P.Value;
677 ContentType = BodyContentType,
678 TransferDecoded = EncodedBody
682 return await this.SendMessage(From, To, Subject, Alternatives,
null);
695 return this.SendMessage(From, To, Subject,
string.Empty, Alternatives, Attachments);
712 byte[] BodyBin = P.Key;
713 string ContentType = P.Value;
715 if (!(Attachments is
null) && Attachments.Length > 0)
720 ContentType = ContentType,
723 Array.Copy(Attachments, 0, Mixed, 1, Attachments.Length);
727 ContentType = P.Value;
730 DateTime Now = DateTime.Now;
731 List<KeyValuePair<string, string>> Headers =
new List<KeyValuePair<string, string>>()
733 new KeyValuePair<string, string>(
"MIME-VERSION",
"1.0"),
734 new KeyValuePair<string, string>(
"FROM", From),
735 new KeyValuePair<string, string>(
"TO", To),
736 new KeyValuePair<string, string>(
"SUBJECT", Subject),
738 new KeyValuePair<string, string>(
"IMPORTANCE",
"normal"),
739 new KeyValuePair<string, string>(
"X-PRIORITY",
"3"),
740 new KeyValuePair<string, string>(
"MESSAGE-ID",
string.IsNullOrEmpty(MessageId) ? Guid.NewGuid().ToString() : MessageId),
741 new KeyValuePair<string, string>(
"CONTENT-TYPE", ContentType)
744 return await this.SendMessage(From, To, Headers.ToArray(), BodyBin, Now);
759 byte[] Data, DateTime Start)
763 throw new ArgumentException(
"Invalid mail address: " + To, nameof(To));
771 if (this.useRelayServer)
773 Host = this.relayHost;
774 Port = this.relayPort;
775 UserName = this.relayUserName;
776 Password = this.relayPassword;
781 if (Exchanges is
null || Exchanges.Length == 0)
782 throw new ArgumentException(
"No mail exchange at " + Domain +
".", nameof(To));
785 Port = DefaultSmtpPort;
790 return await this.SendMessage(Domain, Host, Port, UserName, Password, From, To, Headers, Data, Start);
804 public async Task<bool>
SendMessage(
string Domain,
string Host,
int Port,
string UserName,
string Password,
807 Exception Exception =
null;
813 this.GetSniffer(Host +
" OUT")))
816 await Client.
EHLO(await GetSalutation(this.domain));
819 await Client.
DATA(Headers, Data);
825 catch (TimeoutException ex)
845 Log.
Error(
"Unable to send message.\r\n\r\n" + Exception?.Message,
846 new KeyValuePair<string, object>(
"From", From),
847 new KeyValuePair<string, object>(
"To", To));
854 DateTime TP = DateTime.Now;
855 double Minutes = (TP - Start).TotalMinutes;
857 if (Minutes >= 24 * 60)
861 TP = TP.AddMinutes(1);
862 else if (Minutes < 60)
863 TP = TP.AddMinutes(5);
865 TP = TP.AddMinutes(15);
870 Scheduler.
Add(TP, this.Resend,
new object[] { Domain, Host, Port, UserName, Password, From, To, Headers, Data, Start });
875 private async
void Resend(
object State)
879 object[] P = (
object[])State;
880 string Domain = (string)P[0];
881 string Host = (string)P[1];
882 int Port = (int)P[2];
883 string UserName = (string)P[3];
884 string Password = (string)P[4];
887 KeyValuePair<string, string>[] Headers = (KeyValuePair<string, string>[])P[7];
888 byte[] Data = (
byte[])P[8];
889 DateTime Start = (DateTime)P[9];
891 await this.SendMessage(Domain, Host, Port, UserName, Password, From, To, Headers, Data, Start);
906 if (salutationExpires < DateTime.Now)
909 if (salutation is
null)
913 foreach (NetworkInterface Interface
in NetworkInterface.GetAllNetworkInterfaces())
915 if (Interface.OperationalStatus != OperationalStatus.Up)
918 IPInterfaceProperties Properties = Interface.GetIPProperties();
920 foreach (UnicastIPAddressInformation UnicastAddress
in Properties.UnicastAddresses)
922 if (UnicastAddress.Address.AddressFamily == AddressFamily.InterNetwork && Socket.OSSupportsIPv4)
924 if (IsPublicAddress(UnicastAddress.Address))
928 string AddrStr = UnicastAddress.Address.ToString();
931 foreach (
string Name
in Names)
937 foreach (IPAddress Addr
in Addresses)
939 if (Addr.ToString() == AddrStr)
946 if (!(salutation is
null))
955 if (!(salutation is
null))
966 if (!(salutation is
null))
975 if (salutation is
null)
976 salutation = DefaultDomain;
978 salutationExpires = DateTime.Now.AddHours(1);
991 if (Address.AddressFamily == AddressFamily.InterNetwork && Socket.OSSupportsIPv4)
993 byte[] Addr = Address.GetAddressBytes();
998 else if (Addr[0] == 10)
1001 else if (Addr[0] == 172 && Addr[1] >= 16 && Addr[1] <= 31)
1004 else if (Addr[0] == 192 && Addr[1] == 168)
1007 else if (Addr[0] == 169 && Addr[1] == 254)
Helps with parsing of commong data types.
static string EncodeRfc822(DateTime Timestamp)
Encodes a date and time, according to RFC 822 §5.
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.
Represents alternative versions of the same content, encoded with multipart/alternative
Represents content embedded in other content.
Represents mixed content, encoded with multipart/mixed
Static class managing the application event log. Applications and services log events on this static ...
static void Exception(Exception Exception, string Object, string Actor, string EventId, EventLevel Level, string Facility, string Module, params KeyValuePair< string, object >[] Tags)
Logs an exception. Event type will be determined by the severity of the exception.
static void Error(string Message, string Object, string Actor, string EventId, EventLevel Level, string Facility, string Module, string StackTrace, params KeyValuePair< string, object >[] Tags)
Logs an error event.
Implements a binary TCP Client, by encapsulating a TcpClient. It also makes the use of TcpClient safe...
void Bind()
Binds to a TcpClient that was already connected when provided to the constructor.
void Continue()
Continues reading from the socket, if paused in an event handler.
Simple base class for classes implementing communication protocols.
DNS resolver, as defined in:
static async Task< string[]> LookupDomainName(IPAddress Address)
Looks up the domain name pointing to a specific IP address.
static async Task< string[]> LookupMailExchange(string DomainName)
Looks up the Mail Exchanges related to a given domain name.
static async Task< IPAddress[]> LookupIP4Addresses(string DomainName)
Looks up the IPv4 addresses related to a given domain name.
Module that controls the life cycle of communication.
static bool Stopping
If the system is stopping.
Base class for temporary SMTP-related exceptions.
Client Connection event argument.
Class managing a connection.
Event arguments for SMTP Message events.
Represents one message received over SMTP
Implements a simple SMTP Server, as defined in:
const int DefaultSmtpRelayPort
Secondary SMTP Port (587).
EventHandlerAsync< ClientConnectionEventArgs > ClientConnectionRemoved
Event raised when a client connection has been removed.
static bool IsPublicAddress(IPAddress Address)
Checks if an IPv4 address is public.
SmtpServer(CaseInsensitiveString Domain, int MaxMessageSize, X509Certificate ServerCertificate, bool EncryptionRequired, ISaslPersistenceLayer PersistenceLayer, string[] Ip4DnsBlackLists, string[] Ip6DnsBlackLists, SpfExpression[] SpfExpressions)
Creates an instance of an SMTP server.
static async Task< string > GetSalutation(string DefaultDomain)
Gets the proper salutation name for the server.
async Task< bool > SendMessage(CaseInsensitiveString From, CaseInsensitiveString To, string Subject, object[] AlternativeBodies)
Sends a mail message
async Task< bool > SendMessage(string Domain, string Host, int Port, string UserName, string Password, CaseInsensitiveString From, CaseInsensitiveString To, KeyValuePair< string, string >[] Headers, byte[] Data, DateTime Start)
Sends a mail message
CommunicationLayer ExternalSniffers
External Sniffers for SMTP communication.
const int DefaultConnectionBacklog
Default Connection backlog (10).
int NrClientConnections
Number of client connections.
async Task< bool > SendMessage(CaseInsensitiveString From, CaseInsensitiveString To, string Subject, string MessageId, EmbeddedContent[] Alternatives, EmbeddedContent[] Attachments)
Sends a mail message
SmtpServer(CaseInsensitiveString Domain, int[] Ports, int MaxMessageSize, X509Certificate ServerCertificate, bool EncryptionRequired, ISaslPersistenceLayer PersistenceLayer, string[] Ip4DnsBlackLists, string[] Ip6DnsBlackLists, SpfExpression[] SpfExpressions)
Implements an SMTP server.
EventHandlerAsync< ClientConnectionEventArgs > ClientConnectionAdded
Event raised when a client connection has been added.
CaseInsensitiveString Domain
Domain name.
int[] OpenPorts
Ports successfully opened.
bool EncryptionRequired
If C2S encryption is requried.
void UpdateCertificate(X509Certificate ServerCertificate)
Updates the server certificate
Task< bool > SendMessage(CaseInsensitiveString From, CaseInsensitiveString To, string Subject, EmbeddedContent[] Alternatives, EmbeddedContent[] Attachments)
Sends a mail message
void SetRelaySettings(bool UseRelayServer, string HostName, int PortNumber, string UserName, string Password, string[] RelayDomains, bool LockSettings)
Sets mail relay settings.
void Dispose()
IDisposable.Dispose
async Task< bool > SendMessage(CaseInsensitiveString From, CaseInsensitiveString To, KeyValuePair< string, string >[] Headers, byte[] Data, DateTime Start)
Sends a mail message
string SmtpSnifferPath
If separate sniffers are to be created for each connected client, set this property to the file path ...
SmtpServer(CaseInsensitiveString Domain, int Port, int MaxMessageSize, X509Certificate ServerCertificate, bool EncryptionRequired, ISaslPersistenceLayer PersistenceLayer, string[] Ip4DnsBlackLists, string[] Ip6DnsBlackLists, SpfExpression[] SpfExpressions)
Creates an instance of an SMTP server.
const string SmtpRelayPrivilegeID
SmtpRelay
bool CanRelayForDomain(string Domain)
If the server is permitted to relay messages from a particular domain.
X509Certificate ServerCertificate
Server domain certificate.
const int DefaultSmtpPort
Default SMTP Port (25).
const int DefaultBufferSize
Default buffer size (16384).
async Task Connect()
Connects to the server.
async Task< string > EHLO(string Domain)
Sends the EHLO command.
async Task QUIT()
Executes the QUIT command.
async Task MAIL_FROM(string Sender)
Executes the MAIL FROM command.
async Task DATA(KeyValuePair< string, string >[] Headers, byte[] Body)
Executes the DATA command.
async Task RCPT_TO(string Receiver)
Executes the RCPT TO command.
Sniffer that stores events in memory.
Outputs sniffed data to an XML file.
string FileName
File Name.
string Transform
Transform to use.
Represents a case-insensitive string.
int IndexOf(CaseInsensitiveString value, StringComparison comparisonType)
Reports the zero-based index of the first occurrence of the specified string in the current System....
CaseInsensitiveString Trim()
Removes all leading and trailing white-space characters from the current CaseInsensitiveString object...
CaseInsensitiveString Substring(int startIndex, int length)
Retrieves a substring from this instance. The substring starts at a specified character position and ...
Static interface for database persistence. In order to work, a database provider has to be assigned t...
static string WildcardToRegex(string s, string Wildcard)
Converts a wildcard string to a regular expression string.
Implements an in-memory cache.
void Dispose()
IDisposable.Dispose
int Count
Number of items in cache
bool Remove(KeyType Key)
Removes an item from the cache.
bool TryGetValue(KeyType Key, out ValueType Value)
Tries to get a value from the cache.
KeyType[] GetKeys()
Gets all available keys in the cache.
void Add(KeyType Key, ValueType Value)
Adds an item to the cache.
void Clear()
Clears the cache.
Event arguments for cache item removal events.
ValueType Value
Value of item that was removed.
Static class that dynamically manages types and interfaces available in the runtime environment.
static bool TryGetModuleParameter(string Name, out object Value)
Tries to get a module parameter value.
Class that can be used to schedule events in time. It uses a timer to execute tasks at the appointed ...
DateTime Add(DateTime When, ScheduledEventCallback Callback, object State)
Adds an event.
Contains information about a SPF string.
Interface for XMPP Server persistence layers. The persistence layer should implement caching.
Interface for sniffers. Sniffers can be added to ICommunicationLayer classes to eavesdrop on communic...
BinaryPresentationMethod
How binary data is to be presented.