diff --git a/src/Peachpie.Library/Mail.cs b/src/Peachpie.Library/Mail.cs index dc78b2a0a6..54b6ecd647 100644 --- a/src/Peachpie.Library/Mail.cs +++ b/src/Peachpie.Library/Mail.cs @@ -1,10 +1,17 @@ using System; +using System.Buffers.Text; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Net; +using System.Net.Http.Headers; using System.Net.Mail; +using System.Net.Security; using System.Net.Sockets; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices.ComTypes; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -839,6 +846,798 @@ public void SendMessage(string from, string to, string subject, string headers, //[PhpExtension("IMAP")] // uncomment when the extension is ready public static class Imap { + #region Constants + readonly static Encoding ISO_8859_1 = Encoding.GetEncoding("ISO-8859-1"); + + public const int CL_EXPUNGE = 32768; + #endregion + + #region ImapResource + /// + /// Base of protocols POP3, IMAP and NNTP. + /// + internal abstract class MailResource : PhpResource + { + private enum Service { IMAP, NNTP, POP3}; + + /// + /// Represents a connection between a client and server. + /// + protected Stream _stream; + /// + /// Represents an initial connection string. + /// + protected MailBoxInfo _info; + + #region Contructors + protected MailResource() : base("imap") { } + + /// + /// Creates a client for one of three supported protocols. + /// + /// A connection string, which contains info about desired protocol. Default is IMAP. + /// The client of desired protocol or NULL if there is a problem with the connection. + public static MailResource Create(MailBoxInfo info) + { + if (info == null) + return null; + + MailResource result = null; + Stream stream = GetStream(info); + if (stream == null) + return null; + + if (String.IsNullOrEmpty(info.Service)) + { + if (info.NameFlags.Contains("imap") || info.NameFlags.Contains("imap2") || info.NameFlags.Contains("imap2bis") + || info.NameFlags.Contains("imap4") || info.NameFlags.Contains("imap4rev1")) + result = CreateImap(info, stream); + else if (info.NameFlags.Contains("pop3")) + result = CreatePop3(info, stream); + else if (info.NameFlags.Contains("nntp")) + result = CreateNntp(info, stream); + else // Default is imap + result = CreateImap(info, stream); + } + else + { + + switch (info.Service) + { + case "pop3": + result = CreatePop3(info, stream); + break; + case "nntp": + result = CreateNntp(info, stream); + break; + default: // Default is imap + result = CreateImap(info, stream); + break; + } + } + + return result; + } + + /// + /// Creates IMAP client with a specific type of connection(TLS, SSL, ...). + /// + /// Info can contain information about a type of connection(security, tls, ...) + /// A stream which is connected to a server. + /// Returns the client or null, if there is problem with the connection. + private static ImapResource CreateImap(MailBoxInfo info, Stream stream) + { + if (info == null || stream == null) + return null; + + ImapResource resource = ImapResource.Create(GetStream(info)); + if (resource == null) + return null; + + resource._info = info; // Save info about connection + + if (info.NameFlags.Contains("secure")) // StartTLS with validation + resource.StartTLS(true); + else if (info.NameFlags.Contains("tls"))// StartTLS + resource.StartTLS(!info.NameFlags.Contains("novalidate-cert")); + + return resource; + } + + /// + /// Creates POP3 client with a specific type of connection(TLS, SSL, ...) + /// + /// Info can contain information about a type of connection(security, tls, ...) + /// A stream which is connected to a server. + /// Returns the client or null, if there is problem with the connection. + private static POP3Resource CreatePop3(MailBoxInfo info, Stream stream) + { + if (info == null || stream == null) + return null; + + POP3Resource resource = POP3Resource.Create(GetStream(info)); + if (resource == null) + return null; + + resource._info = info; // Save info about connection + + if (info.NameFlags.Contains("secure")) // StartTLS with validation + resource.StartTLS(true); + else if (info.NameFlags.Contains("tls"))// StartTLS + resource.StartTLS(!info.NameFlags.Contains("novalidate-cert")); + + return resource; + } + + /// + /// Creates NNTP client with a specific type of connection(TLS, SSL, ...) + /// + /// Info can contain information about a type of connection(security, tls, ...) + /// A stream which is connected to a server. + /// Returns the client or null, if there is problem with the connection. + private static ImapResource CreateNntp(MailBoxInfo info, Stream stream) + { + if (info == null || stream == null) + return null; + + //TODO: Support for NNTP protocol + throw new NotImplementedException(); + } + + /// + /// Creates a stream which is connected to a server. Makes no-secure conection by default. + /// + /// Info which contains info about server address + /// Stream or null, if there is a problem with a connetion. + private static Stream GetStream(MailBoxInfo info) + { + if (info == null) + return null; + + TcpClient client; + try + { + client = new TcpClient(info.Hostname, info.Port); + } + catch (Exception ex) + { + if (ex is ArgumentNullException || ex is SocketException) + return null; + + throw; + } + + + if (info.NameFlags.Contains("notls")) // There is nothing to do yet and return non-secure connection (can be later change by starttls command) + return client.GetStream(); + + if (info.NameFlags.Contains("ssl")) + return MakeSslConection(client.GetStream(), info); + + return client.GetStream(); // Makes no-secure conection by default. + } + + /// + /// Makes authentication and ssl conection according to settings in info. + /// + /// A stream which is connected to server. + /// The information about connection string. + /// Sslstream or null, if there is a problem with authentication. + protected static SslStream MakeSslConection(Stream stream, MailBoxInfo info) + { + SslStream result; + try + { + if (info.NameFlags.Contains("novalidate-cert")) + result = new SslStream(stream, false, (sender, certificate, chain, sslPolicyErrors) => true); + else // Validate Certificate + result = new SslStream(stream, false, ValidateServerCertificate); + + result.AuthenticateAsClient(info.Hostname); + } + catch (AuthenticationException) + { + return null; + } + + return result; + } + + /// + /// Validates a certificate. Copied from https://docs.microsoft.com/en-us/dotnet/api/system.net.security.sslstream?view=netcore-3.1. + /// + /// Returns true if is equal to None, false otherwise. + protected static bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + { + if (sslPolicyErrors == SslPolicyErrors.None) + return true; + + // Refuse connection + return false; + } + #endregion + + #region Methods + /// + /// Receive bytes. The bytes represents responses from server and has to be ended \r\n. + /// + /// Set false, if you don't want to wait until response arrived. + /// Returns bytes ended by \r\n, or null if there is no response and you don't want to wait for response. + protected byte[] ReceiveBytes(bool wait = true) + { + byte[] buffer = new byte[2]; + int length = 0; + + if (!wait) + { + _stream.ReadTimeout = 2; + try + { + length = _stream.Read(buffer, 0, buffer.Length); + } + catch (TimeoutException) + { + return null; + } + finally + { + _stream.ReadTimeout = -1; + } + } + else + { + length = _stream.Read(buffer, 0, buffer.Length); + } + + //Wait for complete message. + while (buffer[length - 2] != '\r' || buffer[length - 1] != '\n') + { + Task.Delay(1); + int bufferSize = 1024; + + byte[] newBuffer = new byte[buffer.Length + bufferSize]; + length = _stream.Read(newBuffer, buffer.Length, bufferSize); + Buffer.BlockCopy(buffer, 0, newBuffer, 0, buffer.Length); + length = length + buffer.Length; + buffer = newBuffer; + } + + return buffer; + } + protected abstract bool StartTLS(bool sslValidation); + public abstract bool Login(string username, string password); + public abstract bool Close(); + #endregion + } + + /// + /// Context of POP3 session. + /// + internal class POP3Resource : MailResource + { + /// + /// Represents a status of received message. There are two types in POP3. + /// + private enum Status { OK, ERR, None}; + + /// + /// Represents a response from a server. + /// + class POP3Response + { + /// + /// Status of sent command. + /// + public Status Status { get; set; } = Status.None; + /// + /// The rest of message. + /// + public string Body { get; set; } = null; + /// + /// Complete message delimeted by \r\n sequence(Common ending sequence in POP3). + /// + public byte[] Raw { get; set; } = null; + /// + /// Tries to parse a server response. + /// + /// Buffer, which contains one message ended by \r\n. + /// The result. + /// If the header hasn't the standard form, Only Raw property is filled. + public static bool TryParse(byte[] buffer, out POP3Response response) + { + response = new POP3Response(); + if (buffer == null) + return false; + + int index = 0; + + // Status + if (buffer.Length >= 3 && buffer[index] == OkTag[0] && buffer[index + 1] == OkTag[1] && buffer[index + 2] == OkTag[2]) + { + response.Status = Status.OK; + index = index + 3; + } + else if (buffer.Length >= 4 && buffer[index] == ErrTag[0] && buffer[index + 1] == ErrTag[1] && buffer[index + 2] == ErrTag[2] && buffer[index + 3] == ErrTag[3]) + { + response.Status = Status.ERR; + index = index + 4; + } + else + { + response.Status = Status.None; + } + + if (index < buffer.Length && buffer[index] == ' ') + index++; + + // Body + response.Body = Encoding.ASCII.GetString(buffer, index, buffer.Length - index); + response.Raw = buffer; + + return true; + } + } + + #region Constants + const string OkTag = "+OK"; + const string ErrTag = "-ERR"; + #endregion + + #region Constructors + + /// + /// Creates IMAP client. + /// + /// A stream which is connected to server. + /// Returns the client or null if there is problem with receiving an initial message. + public static POP3Resource Create(Stream stream) + { + POP3Resource resource = new POP3Resource(); + resource._stream = stream; + + List responses = resource.Receive(); + + return (responses != null && responses.Count != 0 && responses[0].Status == Status.OK) ? resource : null; + } + #endregion + + #region Methods + /// + /// Executes the command STLS. + /// + /// Set false if you don't want certificate validation. + /// Returns true on success, false otherwise. + protected override bool StartTLS(bool sslValidation) + { + string command = $"STLS\r\n"; + Write(command); + + List responses = Receive(); + if (responses[0].Status != Status.OK) //It should be the first message. + return false; + + var stream = MakeSslConection(_stream, _info); + if (stream == null) + { + return false; + } + else + { + _stream = stream; + return true; + } + } + + /// + /// Writes command into stream. + /// + /// An POP3 command. + private void Write(string command) + { + _stream.Write(Encoding.ASCII.GetBytes(command)); + } + + /// + /// Receives a response from server. Server can send more than one response. + /// + /// Set false, if you don't want to wait until response arrived. + /// Returns response(s), or null if there is no response and you don't want to wait for response. + private List Receive(bool wait = true) + { + byte[] buffer = ReceiveBytes(wait); + if (buffer == null) + return null; + + List responses = new List(); + int startIndex = 0; + for (int i = 1; i < buffer.Length; i++) + { + if (buffer[i] == '\n' && buffer[i - 1] == '\r') // Split the message (Server's responses are ended by \r\n sequence) + { + if (POP3Response.TryParse(buffer.Slice(startIndex, i - startIndex + 1), out POP3Response pop3)) + responses.Add(pop3); + + startIndex = i + 1; + } + } + + return responses; + } + + /// + /// Executes commands USER {username} and PASS {password.} + /// + /// Returns true on success or false on failure. + public override bool Login(string username, string password) + { + Write($"USER {username}\r\n"); + + List responses = Receive(); + if (responses == null || responses.Count == 0 || responses[0].Status != Status.OK) + return false; + + Write($"PASS {password}\r\n"); + + responses = Receive(); + return ((responses != null || responses.Count != 0) && responses[0].Status == Status.OK); + } + + /// + /// Executes QUIT and calls FreeManaged. + /// + public override bool Close() + { + Write($"QUIT\r\n"); + + List responses = Receive(); + bool result = ((responses != null || responses.Count != 0) && responses[0].Status == Status.OK); + + if (result) + FreeManaged(); + + return result; + } + + protected override void FreeManaged() + { + _stream.Close(); + _stream.Dispose(); + base.FreeManaged(); + } + #endregion + } + + /// + /// Context of NNTP session. + /// + internal class NNTPResource : MailResource + { + public override bool Close() + { + throw new NotImplementedException(); + } + + public override bool Login(string username, string password) + { + throw new NotImplementedException(); + } + + protected override bool StartTLS(bool sslValidation) + { + throw new NotImplementedException(); + } + } + + /// + /// Context of IMAP session. + /// + internal class ImapResource : MailResource + { + /// + /// Represents a status of received message. There are three types in IMAP. + /// + private enum Status { OK, NO, BAD, None }; + + /// + /// Represents a response from a server. + /// + class ImapResponse + { + /// + /// Tag used by IMAP for monitoring status of a command. + /// + public string Tag { get; set; } = null; + /// + /// Status of sent command. + /// + public Status Status { get; set; } = Status.None; + /// + /// The rest of message. + /// + public string Body { get; set; } = null; + /// + /// Complete message delimeted by \r\n sequence(Common ending sequence in IMAP). + /// + public byte[] Raw { get; set; } = null; + + /// + /// Tries to parse a server response. + /// + /// Buffer, which contains one message ended by \r\n. + /// The result. + /// If the header hasn't the standard form, Only Raw property is filled. + public static bool TryParse(byte[] buffer, out ImapResponse response) + { + response = new ImapResponse(); + if (buffer == null) + return false; + + int index = 0; + + // Tag property + if (buffer[index] == UnTaggedTag) + { + response.Tag = UnTaggedTag.ToString(); + index++; + } + else if (buffer[index] == ContinousTag) + { + response.Tag = ContinousTag.ToString(); + index++; + } + else if (buffer[index] == TagPrefix) + { + index++; + while (index < buffer.Length && buffer[index] >= '0' && buffer[index] <= '9') + index++; + + response.Tag = Encoding.ASCII.GetString(buffer, 0, index); + } + + if (index < buffer.Length && buffer[index] == ' ') + index++; + + // Status property + if (index + 1 < buffer.Length) + { + if (buffer[index] == 'N' && buffer[index + 1] == 'O') + { + response.Status = Status.NO; + index += 2; + } + else if (buffer[index] == 'O' && buffer[index + 1] == 'K') + { + response.Status = Status.OK; + index += 2; + } + else if (index + 2 < buffer.Length && buffer[index] == 'D' && buffer[index + 1] == 'A' && buffer[index + 2] == 'D') + { + response.Status = Status.BAD; + index += 3; + } + else + response.Status = Status.None; + } + + // Body property + response.Body = Encoding.ASCII.GetString(buffer, index, buffer.Length - index); + + // Raw property + response.Raw = buffer; + + return true; + } + } + + #region Constants + // Tags belong to responses from a server. + const char UnTaggedTag = '*'; + const char ContinousTag = '+'; + const char TagPrefix = 'A'; + #endregion + + #region Properties + /// + /// Represents next number of tag, which will be used to send new message to server in format {TagPrefix}{_tag}. + /// + private int _tag = 0; + #endregion + + #region Constructors + private ImapResource() {} + + /// + /// Creates IMAP client. + /// + /// A stream which is connected to server. + /// Returns the client or null if there is problem with receiving an initial message. + public static ImapResource Create(Stream stream) + { + ImapResource resource = new ImapResource(); + resource._stream = stream; + + // The server should send an initial message. + List responses = resource.Receive(); + if (responses == null || responses.Count == 0) + return null; + + // First message should contain information about connection status. + return (responses[0].Status == Status.OK) ? resource : null; + } + #endregion + + #region Methods + + /// + /// Executes the command STARTTLS. + /// + /// Set false if you don't want certificate validation. + /// Returns true on success, false otherwise. + protected override bool StartTLS(bool sslValidation = true) + { + string messageTag = $"{TagPrefix}{_tag.ToString()}"; + string command = $"{messageTag} STARTTLS\r\n"; + Write(command); + + bool completed = false; + while (!completed) // Waits until command is completed(Server sends OK message with right messageTag) + { + List responses = Receive(); + if (responses == null || responses.Count == 0) + continue; + + /* Server can send more then one messages. + * We have to find the one, which contains information about command status. + * */ + foreach (var response in responses) + if (response.Tag == messageTag) // There has to be message with right tag. + if (response.Status != Status.OK) // Returns, if command failed. + return false; + else + completed = true; + } + + var stream = MakeSslConection(_stream, _info); + if (stream == null) + { + return false; + } + else + { + _stream = stream; + return true; + } + } + + /// + /// Writes command into stream and increment the tag. + /// + /// An IMAP command. + private void Write(string command) + { + _stream.Write(Encoding.ASCII.GetBytes(command)); + _tag++; + } + + /// + /// Receives a response from server. Server can send more than one response. + /// + /// Set false, if you don't want to wait until response arrived. + /// Returns response(s), or null if there is no response and you don't want to wait for response. + private List Receive(bool wait = true) + { + byte[] buffer = ReceiveBytes(wait); + if (buffer == null) + return null; + + List responses = new List(); + int startIndex = 0; + for (int i = 1; i < buffer.Length; i++) + { + if (buffer[i] == '\n' && buffer[i - 1] == '\r') // Split the message (Server's responses are ended by \r\n sequence) + { + if (ImapResponse.TryParse(buffer.Slice(startIndex, i - startIndex + 1), out ImapResponse imap)) + responses.Add(imap); + + startIndex = i + 1; + } + } + + return responses; + } + + /// + /// Executes command LOGIN {username} {password}. + /// + /// Returns true on success or false on failure. + public override bool Login(string username, string password) + { + string messageTag = $"{TagPrefix}{_tag.ToString()}"; + Write($"{messageTag} LOGIN {username} {password}\r\n"); + + while (true) // Waits for the response. + { + List responses = Receive(); + foreach (var response in responses) + if (response.Tag == messageTag) + return response.Status == Status.OK; + } + } + + /// + /// Excutes command SELECT {path}. + /// + /// The path in mailbox. + /// Returns true on success or false on failure. + public bool Select(string path) + { + string messageTag = $"{TagPrefix}{_tag.ToString()}"; + Write($"{messageTag} SELECT {path}\r\n"); + + while (true) // Waits for the response. + { + List responses = Receive(); + foreach (var response in responses) + if (response.Tag == messageTag) + return response.Status == Status.OK; + } + } + + /// + /// Executes LOGOUT and calls FreeManaged. + /// + public override bool Close() + { + string messageTag = $"{TagPrefix}{_tag.ToString()}"; + Write($"{messageTag} LOGOUT\r\n"); + + bool result = false; + bool completed = false; + while (!completed) // Waits for the response. + { + List responses = Receive(); + foreach (var response in responses) + if (response.Tag == messageTag) + { + result = response.Status == Status.OK; + completed = true; + break; + } + } + + if (result) + FreeManaged(); + + return result; + } + + protected override void FreeManaged() + { + _stream.Close(); + _stream.Dispose(); + base.FreeManaged(); + } + #endregion + } + #endregion + + #region Unsorted + /// + /// Gets instance of or null. + /// If given argument is not an instance of , PHP warning is reported. + /// + static MailResource ValidateMailResource(PhpResource context) + { + if (context is MailResource h && h.IsValid) + { + return h; + } + + // + PhpException.Throw(PhpError.Warning, Resources.Resources.invalid_context_resource); + return null; + } + /// /// Parses an address string. /// @@ -870,5 +1669,453 @@ public static PhpArray imap_rfc822_parse_adrlist(string addresses, string defaul // return arr; } + #endregion + + #region encode,decode + + #region utf7 + /// + /// Transforms bytes to modified UTF-7 text as defined in RFC 2060 + /// + private static string TransformUTF8ToUTF7Modified(byte[] bytes) + { + if (bytes == null || bytes.Length == 0) + return string.Empty; + + var builder = StringBuilderUtilities.Pool.Get(); + + for (int i = 0; i < bytes.Length; i++) + { + // Chars from 0x20 to 0x7e are unchanged excepts "&" which is replaced by "&-". + if (bytes[i] >= 0x20 && bytes[i] <= 0x7e) + { + if (bytes[i] == 0x26) + builder.Append("&-"); + else + builder.Append((char)bytes[i]); + } + else // Collects all bytes until Char from 0x20 to 0x7eis reached. + { + int index = i; + while ( i < bytes.Length && (bytes[i] < 0x20 || bytes[i] > 0x7e)) + i++; + + //Add bytes to stringbuilder + //builder.Append("&" + Encoding.UTF8.GetString(bytes, index, i - index).Replace("/", ",") + "-"); + builder.Append("&" + System.Convert.ToBase64String(bytes, index, i - index).Replace("/", ",") + "-"); + + if (i < bytes.Length) + i--; + } + } + + return StringBuilderUtilities.GetStringAndReturn(builder); + } + + private static string TransformUTF7ModifiedToUTF8(Context ctx, string text) + { + if (string.IsNullOrEmpty(text)) + return null; + + var builder = StringBuilderUtilities.Pool.Get(); + + for (int i = 0; i < text.Length; i++) + { + if (text[i] == '&') + { + //if (i == text.Length - 1) + // ; // Error + + if (text[++i] == '-') // Means "&" char. + builder.Append("&"); + else // Shift + { + int index = i; + while (i < text.Length && text[i] != '-') + i++; + + string encode = text.Substring(index, i - index); + if (encode.Length % 4 != 0) + encode = encode.PadRight(encode.Length + (4 - encode.Length % 4), '='); + + builder.Append(Encoding.UTF7.GetString(System.Convert.FromBase64String(encode.Replace(",","/")))); + } + } + else if (text[i] >= 0x20 && text[i] <= 0x7e) + { + builder.Append(text[i]); + } + else + { + //Error + } + } + + return StringBuilderUtilities.GetStringAndReturn(builder); + } + + /// + /// Converts ctx.StringEncoding encoding to UTF-7 modified(used in IMAP) see RFC 2060. + /// + private static PhpString ToUTF7Modified(Context ctx, string text) + { + if (string.IsNullOrEmpty(text)) + return PhpString.Empty; + + using MemoryStream stream = new MemoryStream(); + using BinaryWriter writer = new BinaryWriter(stream); + + byte[] ampSequence = new byte[] { 0x26, 0x2D }; // Means characters '&' and '-'. + + for (int i = 0; i < text.Length; i++) + { + // Chars from 0x20 to 0x7e are unchanged excepts "&" which is replaced by "&-". + if (text[i] >= 0x20 && text[i] <= 0x7e) + { + if (text[i] == 0x26) + writer.Write(ampSequence); + else + writer.Write(text[i]); + } + else // Collects all bytes until Char from 0x20 to 0x7e is reached. + { + int start = i; + while (i < text.Length && (text[i] < 0x20 || text[i] > 0x7e)) + i++; + + string sequence = text.Substring(start, i - start); + // By RFC it shloud be encoded by UTF16BE, but PHP behaves in a different way. + byte[] sequenceEncoded = ctx.StringEncoding.GetBytes(sequence); + + string base64Modified = System.Convert.ToBase64String(sequenceEncoded).Replace('/', ',').Trim('='); + + writer.Write('&'); + writer.Write(Encoding.ASCII.GetBytes(base64Modified)); + writer.Write('-'); + + if (i < text.Length) + i--; + } + } + + writer.Flush(); + return new PhpString(stream.ToArray()); + } + + /// + /// Converts UTF-7 modified(used in IMAP) see RFC 2060 encoding to . + /// + private static PhpString FromUTF7Modified(Context ctx, PhpString text) + { + if (text.IsEmpty) + return string.Empty; + + byte[] utf7Modified = text.ToBytes(ctx.StringEncoding); + + using MemoryStream stream = new MemoryStream(); + using BinaryWriter writer = new BinaryWriter(stream); + + for (int i = 0; i < utf7Modified.Length; i++) + { + if (utf7Modified[i] == '&') + { + if (i == utf7Modified.Length - 1) + throw new FormatException(); // Error + + if (utf7Modified[++i] == '-') // Means "&" char. + { + writer.Write((byte)'&'); + } + else // Shifting + { + int start = i; + while (i < utf7Modified.Length && utf7Modified[i] != '-') + i++; + + string sequence = Encoding.ASCII.GetString(utf7Modified, start, i - start).Replace(',','/'); + + if ((sequence.Length % 4) != 0) // Adds padding + sequence = sequence.PadRight(sequence.Length + 4 - (sequence.Length % 4),'='); + + byte[] base64Decoded = System.Convert.FromBase64String(sequence); + + writer.Write(base64Decoded); + } + } + else if (text[i] >= 0x20 && text[i] <= 0x7e) + { + writer.Write((byte)text[i]); + } + else + { + throw new FormatException(); // Error + } + } + + writer.Flush(); + return new PhpString(stream.ToArray()); + } + + /// + /// Converts ISO-8859-1 string to modified UTF-7 text. + /// + /// The context of script. + /// An ISO-8859-1 string. + /// Returns data encoded with the modified UTF-7 encoding as defined in RFC 2060 + public static PhpString imap_utf7_encode(Context ctx, PhpString data) + { + + + + + + + + + + + + + + return ToUTF7Modified(ctx, data.ToString(ctx)); + } + + /// + /// Decodes modified UTF-7 text into ISO-8859-1 string. + /// + /// + /// A modified UTF-7 encoding string, as defined in RFC 2060 + /// Returns a string that is encoded in ISO-8859-1 and consists of the same sequence of characters in text, + /// or FALSE if text contains invalid modified UTF-7 sequence or + /// text contains a character that is not part of ISO-8859-1 character set. + public static PhpString imap_utf7_decode(Context ctx, PhpString text) + { + return FromUTF7Modified(ctx, text); + } + #endregion + + #region base64 + + /// + /// Decodes the given BASE-64 encoded text. + /// + /// The context of script. + /// The encoded text. + /// Returns the decoded message as a string. + public static string imap_base64(Context ctx, string text) + { + try + { + return ctx.StringEncoding.GetString(Base64Utils.FromBase64(text.AsSpan(), true)); + } + catch (FormatException) + { + + return string.Empty; + } + } + + #endregion + + #endregion + + #region connection, errors, quotas + + /// + /// Represents parsed "conection string" from image_open. + /// + internal class MailBoxInfo + { + public string Hostname { get; set; } + public int Port { get; set; } + public string MailBoxName { get; set; } + public HashSet NameFlags { get; set; } = new HashSet(); + public string Service { get; set; } + public string User { get; set; } + public string Authuser { get; set; } + } + + /// + /// Parses mailbox. + /// + /// The mailbox has the format: "{" remote_system_name [":" port] [flags] "}" [mailbox_name] + /// Parsed information about mailbox. + /// True on Success, False on failure. + static bool TryParseHostName(string mailbox, out MailBoxInfo info) + { + info = new MailBoxInfo(); + if (String.IsNullOrEmpty(mailbox)) + return false; + + int index = 0; + int startSection = index; + + string GetName(string mailbox) + { + int startIndex = index; + while (mailbox.Length > index && ((mailbox[index] >= 'a' && mailbox[index] <= 'z') + || (mailbox[index] >= 'A' && mailbox[index] <= 'Z') || (mailbox[index] >= '0' && mailbox[index] <= '9')) || mailbox[index] == '-') + index++; + + return (index == startIndex) ? null : mailbox.Substring(startIndex, index - startIndex); + } + + // Mandatory char '{' + if (mailbox[index++] != '{') + return false; + + // Finds remote_system_name + startSection = index; + while (mailbox.Length > index && mailbox[index] != '/' && mailbox[index] != ':' && mailbox[index] != '}') + index++; + + if (startSection == index) + return false; + else + info.Hostname = mailbox.Substring(1, index - 1); + + // Finds port number + startSection = index + 1; + if (mailbox[index++] == ':') + { + while (mailbox.Length > index && mailbox[index] >= '0' && mailbox[index] <= '9') + index++; + + if (mailbox[index] != '/' && mailbox[index] != '}' && index == startSection) + return false; + else + info.Port = int.Parse(mailbox.Substring(startSection, index - startSection)); + } + + // Finds flags + startSection = index + 1; + if (mailbox[index] == '/') + { + index++; + while (true) + { + string flag = GetName(mailbox); + + if (String.IsNullOrEmpty(flag)) + return false; + + if (flag == "service" || flag == "user" || flag == "authuser") + { + if (mailbox[index++] != '=') + return false; + + string name = GetName(mailbox); + if (String.IsNullOrEmpty(name)) + return false; + + switch (flag) + { + case "service": + info.Authuser = name; + break; + case "user": + info.User = name; + break; + case "authuser": + info.Authuser = name; + break; + } + } + else + { + info.NameFlags.Add(flag); + } + + if (mailbox.Length <= index || mailbox[index] == '}') + break; + else + startSection = ++index; + } + } + + // Mandatory char '{' + if (mailbox.Length <= index || mailbox[index] != '}') + return false; + + // Finds mailbox box directory. + if (mailbox.Length > ++index) + info.MailBoxName = mailbox.Substring(index, mailbox.Length - index); + + return true; + } + + /// + /// Open an IMAP stream to a mailbox. This function can also be used to open streams to POP3 and NNTP servers, but some functions and features are only available on IMAP servers. + /// + /// A mailbox name consists of a server and a mailbox path on this server. + /// The user name. + /// The password associated with the username. + /// The options are a bit mask of connection options. + /// Number of maximum connect attempts. + /// Connection parameters. + /// Returns an IMAP stream on success or FALSE on error. + [return: CastToFalse] + public static PhpResource imap_open(string mailbox, string username , string password, int options, int n_retries, PhpArray @params) + { + // Unsupported flags: authuser, debug, (nntp - can be done), readonly + // Unsupported options: OP_SECURE, OP_PROTOTYPE, OP_SILENT, OP_SHORTCACHE, OP_DEBUG, OP_READONLY, OP_ANONYMOUS, OP_HALFOPEN, CL_EXPUNGE + // Unsupported n_retries, params + + if (!TryParseHostName(mailbox, out MailBoxInfo info)) + return null; + + if (!String.IsNullOrEmpty(info.User)) + username = info.User; + + if (info.NameFlags.Contains("anonymous")) + username = "ANONYMOUS"; + + try + { + MailResource resource = MailResource.Create(info); + + if (resource == null) + return null; + if (!resource.Login(username, password)) + return null; + + if (resource is ImapResource imap) // There is only one folder in POP3. + { + if (String.IsNullOrEmpty(info.MailBoxName)) + imap.Select("INBOX"); + else + imap.Select(info.MailBoxName); + } + + return resource; + } + catch (SocketException) + { + return null; + } + } + + /// + /// Closes the imap stream. + /// + /// An IMAP stream returned by imap_open(). + /// If set to CL_EXPUNGE, the function will silently expunge the mailbox before closing, removing all messages marked for deletion. You can achieve the same thing by using imap_expunge() + /// Returns TRUE on success or FALSE on failure. + public static bool imap_close(PhpResource imap_stream, int flag = 0) + { + MailResource resource = ValidateMailResource(imap_stream); + if (resource == null) + return false; + + if ((flag & CL_EXPUNGE) == CL_EXPUNGE) + { + //TODO: Call imap_expunge + throw new NotImplementedException(); + } + + resource.Close(); + return true; + } + #endregion } } diff --git a/tests/imap/imap_001.php b/tests/imap/imap_001.php new file mode 100644 index 0000000000..35ee43b439 --- /dev/null +++ b/tests/imap/imap_001.php @@ -0,0 +1,15 @@ +