4using System.Net.NetworkInformation;
6using System.Security.Cryptography.X509Certificates;
8using System.Text.RegularExpressions;
9using System.Threading.Tasks;
71 private static string salutation =
null;
72 private static DateTime salutationExpires = DateTime.MinValue;
74 private LinkedList<TcpListener> listeners =
new LinkedList<TcpListener>();
77 private X509Certificate serverCertificate;
80 private readonly
bool encryptionRequired;
81 private readonly
int maxMessageSize;
82 private readonly
string[] ip4DnsBlackLists;
83 private readonly
string[] ip6DnsBlackLists;
85 private string smtpSnifferPath =
null;
86 private bool disposed =
false;
89 private string[] relayDomains =
null;
90 private Regex[] relayDomainsEx =
null;
91 private string relayHost =
string.Empty;
92 private string relayUserName =
string.Empty;
93 private string relayPassword =
string.Empty;
94 private int relayPort = 587;
95 private bool useRelayServer =
false;
96 private bool relayLocked =
false;
113 string[] Ip4DnsBlackLists,
string[] Ip6DnsBlackLists,
SpfExpression[] SpfExpressions)
115 EncryptionRequired, PersistenceLayer, Ip4DnsBlackLists, Ip6DnsBlackLists, SpfExpressions)
133 string[] Ip4DnsBlackLists,
string[] Ip6DnsBlackLists,
SpfExpression[] SpfExpressions)
135 Ip4DnsBlackLists, Ip6DnsBlackLists, SpfExpressions)
155 this.persistenceLayer = PersistenceLayer;
159 this.maxMessageSize = MaxMessageSize;
160 this.ip4DnsBlackLists = Ip4DnsBlackLists;
161 this.ip6DnsBlackLists = Ip6DnsBlackLists;
162 this.spfExpressions = SpfExpressions;
165 throw new ArgumentException(
"Server Certificate must be provided, if encryption is required.", nameof(
ServerCertificate));
168 this.clientConnections.Removed += this.ClientConnections_Removed;
170 this.Initialize(Ports);
173 private void Initialize(
int[] Ports)
177 TcpListener Listener;
179 foreach (NetworkInterface Interface
in NetworkInterface.GetAllNetworkInterfaces())
181 if (Interface.OperationalStatus != OperationalStatus.Up)
184 IPInterfaceProperties Properties = Interface.GetIPProperties();
186 foreach (UnicastIPAddressInformation UnicastAddress
in Properties.UnicastAddresses)
188 if ((UnicastAddress.Address.AddressFamily == AddressFamily.InterNetwork && Socket.OSSupportsIPv4) ||
189 (UnicastAddress.Address.AddressFamily == AddressFamily.InterNetworkV6 && Socket.OSSupportsIPv6))
191 if (!(Ports is
null))
193 foreach (
int Port
in Ports)
197 this.externalSniffers.Information(
"Opening port " + Port.ToString() +
" on " + UnicastAddress.Address.ToString() +
".");
199 Listener =
new TcpListener(UnicastAddress.Address, Port);
201 Listener.BeginAcceptTcpClient(this.AcceptTcpClientCallback, Listener);
202 this.listeners.AddLast(Listener);
204 this.externalSniffers.Information(
"Port " + Port.ToString() +
" on " + UnicastAddress.Address.ToString() +
" opened.");
208 Log.
Exception(ex, UnicastAddress.Address.ToString() +
":" + Port);
233 get => this.smtpSnifferPath;
234 set => this.smtpSnifferPath = value;
242 await e.
Value.DisposeAsync();
255 get => this.clientConnections.
Count;
264 List<SmtpClientConnection> Connections =
new List<SmtpClientConnection>();
266 foreach (Guid Id
in this.clientConnections.
GetKeys())
269 Connections.Add(Connection);
272 Connections.Sort((c1, c2) =>
274 return c1.UserName.CompareTo(c2.UserName);
277 return Connections.ToArray();
288 return this.clientConnections.
TryGetValue(ID, out Connection);
316 internal string[] Ip4DnsBlackLists => this.ip4DnsBlackLists;
317 internal string[] Ip6DnsBlackLists => this.ip6DnsBlackLists;
318 internal SpfExpression[] SpfExpressions => this.spfExpressions;
325 this.disposed =
true;
327 if (!(this.clientConnections is
null))
329 this.clientConnections.
Clear();
330 this.clientConnections.
Dispose();
331 this.clientConnections =
null;
334 if (!(this.listeners is
null))
336 LinkedList<TcpListener> Listeners = this.listeners;
337 this.listeners =
null;
339 foreach (TcpListener Listener
in Listeners)
343 if (!(this.sniffers is
null))
345 this.sniffers.
Clear();
347 this.sniffers =
null;
350 if (!(this.externalSniffers is
null))
352 foreach (
ISniffer Sniffer
in this.externalSniffers)
353 (Sniffer as IDisposable)?.
Dispose();
364 return this.GetOpenPorts(this.listeners);
368 private int[] GetOpenPorts(LinkedList<TcpListener> Listeners)
370 SortedDictionary<int, bool> Open =
new SortedDictionary<int, bool>();
372 if (!(Listeners is
null))
374 IPEndPoint IPEndPoint;
376 foreach (TcpListener Listener
in Listeners)
378 IPEndPoint = Listener.LocalEndpoint as IPEndPoint;
379 if (!(IPEndPoint is
null))
380 Open[IPEndPoint.Port] =
true;
384 int[] Result =
new int[Open.Count];
385 Open.Keys.CopyTo(Result, 0);
394 private async
void AcceptTcpClientCallback(IAsyncResult ar)
401 TcpListener Listener = (TcpListener)ar.AsyncState;
405 TcpClient Client = Listener.EndAcceptTcpClient(ar);
406 SmtpClientConnection ClientConnection;
409 if (!
string.IsNullOrEmpty(this.smtpSnifferPath))
411 else if (this.externalSniffers.HasSniffers)
412 Sniffers = this.externalSniffers.Sniffers;
419 ClientConnection =
new SmtpClientConnection(
BinaryTcpClient,
this, this.persistenceLayer, this.maxMessageSize, Sniffers);
422 this.clientConnections[ClientConnection.ID] = ClientConnection;
427 await ClientConnection.BeginWrite(
"220 " + this.domain +
" ESMTP Sendmail ...\r\n",
null,
null);
432 Listener.BeginAcceptTcpClient(this.AcceptTcpClientCallback, Listener);
435 catch (SocketException)
439 catch (ObjectDisposedException)
443 catch (NullReferenceException)
449 if (this.listeners is
null)
456 internal string GetTransformPath()
458 foreach (
ISniffer Sniffer
in this.externalSniffers.Sniffers)
467 internal void Closed(SmtpClientConnection Connection)
469 this.clientConnections?.
Remove(Connection.ID);
483 return this.persistenceLayer.GetAccount(UserName);
489 public event EventHandlerAsync<ClientConnectionEventArgs> ClientConnectionAdded =
null;
494 public event EventHandlerAsync<ClientConnectionEventArgs> ClientConnectionRemoved =
null;
503 public event EventHandlerAsync<SmtpMessageEventArgs> MessageReceived =
null;
518 FileName = this.smtpSnifferPath.Replace(
"%ENDPOINT%", Key);
526 internal async Task CacheSniffers(IEnumerable<ISniffer> Sniffers)
528 foreach (
ISniffer Sniffer
in Sniffers)
533 await DisposableAsync.DisposeAsync();
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)
553 return e.
Value.DisposeAsync();
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 this.relayDomainsEx ??=
new Regex[this.relayDomains.Length];
645 if (this.relayDomainsEx[i] is
null)
646 this.relayDomainsEx[i] =
new Regex(
Database.
WildcardToRegex(s,
"*"), RegexOptions.Singleline | RegexOptions.IgnoreCase);
648 Match M = this.relayDomainsEx[i].Match(Domain);
649 if (M.Success && M.Index == 0 && M.Length == Domain.Length)
664 string Subject,
object[] AlternativeBodies)
666 int i, c = AlternativeBodies.Length;
669 for (i = 0; i < c; i++)
675 byte[] EncodedBody = P.
Encoded;
679 ContentType = BodyContentType,
680 TransferDecoded = EncodedBody
684 return await this.SendMessage(From, To, Subject, Alternatives,
null);
698 return this.SendMessage(From, To, Subject,
string.Empty, Alternatives, Attachments);
721 if (!(Attachments is
null) && Attachments.Length > 0)
726 ContentType = ContentType,
729 Array.Copy(Attachments, 0, Mixed, 1, Attachments.Length);
739 DateTime Now = DateTime.Now;
740 List<KeyValuePair<string, string>> Headers =
new List<KeyValuePair<string, string>>()
742 new KeyValuePair<string, string>(
"MIME-VERSION",
"1.0"),
743 new KeyValuePair<string, string>(
"FROM", From),
744 new KeyValuePair<string, string>(
"TO", To),
745 new KeyValuePair<string, string>(
"SUBJECT", Subject),
747 new KeyValuePair<string, string>(
"IMPORTANCE",
"normal"),
748 new KeyValuePair<string, string>(
"X-PRIORITY",
"3"),
749 new KeyValuePair<string, string>(
"MESSAGE-ID",
string.IsNullOrEmpty(MessageId) ? Guid.NewGuid().ToString() : MessageId),
750 new KeyValuePair<string, string>(
"CONTENT-TYPE", ContentType)
753 return await this.SendMessage(From, To, Headers.ToArray(), BodyBin, Now);
767 byte[] Data, DateTime Start)
771 throw new ArgumentException(
"Invalid mail address: " + To, nameof(To));
779 if (this.useRelayServer)
781 Host = this.relayHost;
782 Port = this.relayPort;
783 UserName = this.relayUserName;
784 Password = this.relayPassword;
789 if (Exchanges is
null || Exchanges.Length == 0)
790 throw new ArgumentException(
"No mail exchange at " + Domain +
".", nameof(To));
793 Port = DefaultSmtpPort;
798 return await this.SendMessage(Domain, Host, Port, UserName, Password, From, To, Headers, Data, Start);
815 public async Task<bool>
SendMessage(
string Domain,
string Host,
int Port,
820 Exception Exception =
null;
826 this.GetSniffer(Host +
" OUT"));
828 await Client.Connect();
829 await Client.EHLO(await GetSalutation(this.domain));
830 await Client.MAIL_FROM(From);
831 await Client.RCPT_TO(To);
832 await Client.DATA(Headers, Data);
837 catch (TimeoutException ex)
856 Log.
Error(
"Unable to send message.\r\n\r\n" + Exception?.Message,
857 new KeyValuePair<string, object>(
"From", From),
858 new KeyValuePair<string, object>(
"To", To));
865 DateTime TP = DateTime.Now;
866 double Minutes = (TP - Start).TotalMinutes;
868 if (Minutes >= 24 * 60)
872 TP = TP.AddMinutes(1);
873 else if (Minutes < 60)
874 TP = TP.AddMinutes(5);
876 TP = TP.AddMinutes(15);
881 Scheduler.
Add(TP, this.Resend,
new object[] { Domain, Host, Port, UserName, Password, From, To, Headers, Data, Start });
886 private async
void Resend(
object State)
890 object[] P = (
object[])State;
891 string Domain = (string)P[0];
892 string Host = (string)P[1];
893 int Port = (int)P[2];
894 string UserName = (string)P[3];
895 string Password = (string)P[4];
898 KeyValuePair<string, string>[] Headers = (KeyValuePair<string, string>[])P[7];
899 byte[] Data = (
byte[])P[8];
900 DateTime Start = (DateTime)P[9];
902 await this.SendMessage(Domain, Host, Port, UserName, Password, From, To, Headers, Data, Start);
917 if (salutationExpires < DateTime.Now)
920 if (salutation is
null)
924 foreach (NetworkInterface Interface
in NetworkInterface.GetAllNetworkInterfaces())
926 if (Interface.OperationalStatus != OperationalStatus.Up)
929 IPInterfaceProperties Properties = Interface.GetIPProperties();
931 foreach (UnicastIPAddressInformation UnicastAddress
in Properties.UnicastAddresses)
933 if (UnicastAddress.Address.AddressFamily == AddressFamily.InterNetwork && Socket.OSSupportsIPv4)
935 if (IsPublicAddress(UnicastAddress.Address))
939 string AddrStr = UnicastAddress.Address.ToString();
942 if (!(Names is
null))
944 foreach (
string Name
in Names)
950 if (!(Addresses is
null))
952 foreach (IPAddress Addr
in Addresses)
954 if (Addr.ToString() == AddrStr)
962 if (!(salutation is
null))
972 if (!(salutation is
null))
983 if (!(salutation is
null))
992 salutation ??= DefaultDomain;
994 salutationExpires = DateTime.Now.AddHours(1);
1007 if (Address.AddressFamily == AddressFamily.InterNetwork && Socket.OSSupportsIPv4)
1009 byte[] Addr = Address.GetAddressBytes();
1014 else if (Addr[0] == 10)
1017 else if (Addr[0] == 172 && Addr[1] >= 16 && Addr[1] <= 31)
1020 else if (Addr[0] == 192 && Addr[1] == 168)
1023 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.
Contains information about a response to a content request.
byte[] Encoded
Encoded object.
string ContentType
Internet Content-Type of encoded object.
bool HasError
If an error occurred.
Static class managing encoding and decoding of internet content.
static Task< ContentResponse > 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.
string RemoteEndPoint
Remote End-point of connection. This corresponds to the IP Endpoint of the remote party in normal cas...
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 Task< IPAddress[]> TryLookupIP4Addresses(string DomainName)
Tries to look up the IPv4 addresses related to a given domain name.
static Task< string[]> TryLookupMailExchange(string DomainName)
Tries to look up the Mail Exchanges related to a given domain name.
static Task< string[]> TryLookupDomainName(IPAddress Address)
Tries to look up the domain name pointing to a specific IP address.
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.
bool TryGetClientConnection(Guid ID, out SmtpClientConnection Connection)
Tries to get a client connection, given its identifier.
CaseInsensitiveString Domain
Domain name.
int[] OpenPorts
Ports successfully opened.
bool EncryptionRequired
If C2S encryption is requried.
void UpdateCertificate(X509Certificate ServerCertificate)
Updates the server certificate
SmtpClientConnection[] GetClientConnections()
Gets an array of available client connection.s
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).
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, Action< object > Callback, object State)
Adds an event.
Contains information about a SPF string.
Interface for asynchronously disposable objects.
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...
Interface for Mutual TLS (mTLS) Clients or TLS servers.
BinaryPresentationMethod
How binary data is to be presented.