From dc8d0f9181eeb51d7686b410b03a2d025affb831 Mon Sep 17 00:00:00 2001 From: Tomas Husak Date: Mon, 27 Jul 2020 16:52:53 +0200 Subject: [PATCH 1/8] Implementation encode, decode utf7 --- src/Peachpie.Library/Mail.cs | 123 +++++++++++++++++++++++++++++++++++ tests/imap/imap_001.php | 14 ++++ 2 files changed, 137 insertions(+) create mode 100644 tests/imap/imap_001.php diff --git a/src/Peachpie.Library/Mail.cs b/src/Peachpie.Library/Mail.cs index a9b53bf982..107eb18c6f 100644 --- a/src/Peachpie.Library/Mail.cs +++ b/src/Peachpie.Library/Mail.cs @@ -839,6 +839,14 @@ public void SendMessage(string from, string to, string subject, string headers, //[PhpExtension("IMAP")] // uncomment when the extension is ready public static class Imap { + #region Variables + + readonly static Encoding ISO_8859_1 = Encoding.GetEncoding("ISO-8859-1"); + + #endregion + + + /// /// Parses an address string. /// @@ -870,5 +878,120 @@ public static PhpArray imap_rfc822_parse_adrlist(string addresses, string defaul // return arr; } + + #region encode,decode + + /// + /// 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 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 string imap_utf7_encode(Context ctx, PhpString data) + { + if (data.IsEmpty) + return string.Empty; + + return TransformUTF8ToUTF7Modified(data.ToBytes(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, string text) + { + if (string.IsNullOrEmpty(text)) + return PhpString.Empty; + + return new PhpString(Encoding.Convert(ctx.StringEncoding, ISO_8859_1, ctx.StringEncoding.GetBytes(TransformUTF7ModifiedToUTF8(ctx, text)))); + } + + + #endregion } } diff --git a/tests/imap/imap_001.php b/tests/imap/imap_001.php new file mode 100644 index 0000000000..8010ce894e --- /dev/null +++ b/tests/imap/imap_001.php @@ -0,0 +1,14 @@ + Date: Tue, 28 Jul 2020 12:14:57 +0200 Subject: [PATCH 2/8] version1 --- src/Peachpie.Library/Mail.cs | 115 ++++++++++++++++++++++++++++++++--- tests/imap/imap_001.php | 3 +- 2 files changed, 107 insertions(+), 11 deletions(-) diff --git a/src/Peachpie.Library/Mail.cs b/src/Peachpie.Library/Mail.cs index 107eb18c6f..dd95c9a130 100644 --- a/src/Peachpie.Library/Mail.cs +++ b/src/Peachpie.Library/Mail.cs @@ -5,6 +5,8 @@ using System.Net; using System.Net.Mail; using System.Net.Sockets; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices.ComTypes; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -961,6 +963,105 @@ private static string TransformUTF7ModifiedToUTF8(Context ctx, string text) return StringBuilderUtilities.GetStringAndReturn(builder); } + /// + /// Converts UTF16LE encoding to UTF-7 modified(used in IMAP) see RFC 2060. + /// + private static PhpString UTF16LEToUTF7Modified(Context ctx, string text) + { + if (string.IsNullOrEmpty(text)) + return PhpString.Empty; + + var builder = StringBuilderUtilities.Pool.Get(); + + 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) + builder.Append("&-"); + else + builder.Append(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++; + + byte[] utf16BE = ctx.StringEncoding.GetBytes(text.ToCharArray(), start, i - start); // Contradiction with RFC. + // byte[] utf16BE = Encoding.BigEndianUnicode.GetBytes(text.ToCharArray(), start, i - start); + string base64Modified = System.Convert.ToBase64String(utf16BE).Replace('/', ',').Trim('='); + + builder.Append("&" + base64Modified + "-"); + + if (i < text.Length) + i--; + } + } + + return new PhpString(StringBuilderUtilities.GetStringAndReturn(builder),ctx); + } + + /// + /// Converts UTF-7 modified(used in IMAP) see RFC 2060 encoding to UTF16LE. + /// + private static PhpString UTF7ModifiedToUTF16LE(Context ctx, PhpString text) + { + if (text.IsEmpty) + return string.Empty; + + byte[] utf7Modified = text.ToBytes(ctx.StringEncoding); + var builder = StringBuilderUtilities.Pool.Get(); + + for (int i = 0; i < utf7Modified.Length; i++) + { + if (utf7Modified[i] == '&') + { + if (i == utf7Modified.Length - 1) + throw new Exception(); // Error + + if (utf7Modified[++i] == '-') // Means "&" char. + { + builder.Append("&"); + } + else // Shifting + { + var sectionBuilder = StringBuilderUtilities.Pool.Get(); + while (i < utf7Modified.Length && utf7Modified[i] != '-') + sectionBuilder.Append((char)utf7Modified[i++]); + + //int start = i; + //while (i < utf7Modified.Length && utf7Modified[i] != '-') + // i++; + //string base64Modified = utf7Modified.Substring(start, i - start); + + string base64Modified = StringBuilderUtilities.GetStringAndReturn(sectionBuilder); + + if ((base64Modified.Length % 4) != 0) // Adds padding + base64Modified = base64Modified.PadRight(base64Modified.Length + 4 - (base64Modified.Length % 4),'='); + + base64Modified = base64Modified.Replace(',', '/'); // Replace , + + //Decode Base64 and UTF16BE + var a = System.Convert.FromBase64String(base64Modified); + + builder.Append(ctx.StringEncoding.GetString(System.Convert.FromBase64String(base64Modified))); + //builder.Append(Encoding.BigEndianUnicode.GetString(System.Convert.FromBase64String(base64Modified))); + } + } + else if (text[i] >= 0x20 && text[i] <= 0x7e) + { + builder.Append(text[i]); + } + else + { + throw new Exception(); // Error + } + } + var b = new PhpString(StringBuilderUtilities.GetStringAndReturn(builder),ctx); + return b; + } /// /// Converts ISO-8859-1 string to modified UTF-7 text. @@ -968,12 +1069,9 @@ private static string TransformUTF7ModifiedToUTF8(Context ctx, string 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 string imap_utf7_encode(Context ctx, PhpString data) + public static PhpString imap_utf7_encode(Context ctx, PhpString data) { - if (data.IsEmpty) - return string.Empty; - - return TransformUTF8ToUTF7Modified(data.ToBytes(ctx)); + return UTF16LEToUTF7Modified(ctx, data.ToString(ctx)); } /// @@ -983,12 +1081,9 @@ public static string imap_utf7_encode(Context ctx, PhpString data) /// 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, string text) + public static PhpString imap_utf7_decode(Context ctx, PhpString text) { - if (string.IsNullOrEmpty(text)) - return PhpString.Empty; - - return new PhpString(Encoding.Convert(ctx.StringEncoding, ISO_8859_1, ctx.StringEncoding.GetBytes(TransformUTF7ModifiedToUTF8(ctx, text)))); + return UTF7ModifiedToUTF16LE(ctx, text); } diff --git a/tests/imap/imap_001.php b/tests/imap/imap_001.php index 8010ce894e..35ee43b439 100644 --- a/tests/imap/imap_001.php +++ b/tests/imap/imap_001.php @@ -3,7 +3,8 @@ // Arrange $str = '~peter&/mail/日本語/台北'; -$str_utf7modified = '~peter/mail/&ZeVnLIqe-/&U,BTFw-'; +//$str_utf7modified = '~peter/mail/&ZeVnLIqe-/&U,BTFw-'; +$str_utf7modified = '&ZeVnLIqe-'; // Act $str_utf7_test = imap_utf7_encode($str); From bae0f8e7a180eb675e1e3dcd07c7870fa160ee1f Mon Sep 17 00:00:00 2001 From: Tomas Husak Date: Tue, 28 Jul 2020 13:25:20 +0200 Subject: [PATCH 3/8] Implementation imap_base64 --- src/Peachpie.Library/Mail.cs | 23 +++++++++++++++++++++++ tests/imap/imap_002.php | 15 +++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 tests/imap/imap_002.php diff --git a/src/Peachpie.Library/Mail.cs b/src/Peachpie.Library/Mail.cs index dd95c9a130..8523f07dad 100644 --- a/src/Peachpie.Library/Mail.cs +++ b/src/Peachpie.Library/Mail.cs @@ -1085,7 +1085,30 @@ public static PhpString imap_utf7_decode(Context ctx, PhpString text) { return UTF7ModifiedToUTF16LE(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 Encoding.Unicode.GetString(Base64Utils.FromBase64(text.AsSpan(), true)); + } + catch (FormatException) + { + return string.Empty; + } + } + #endregion #endregion } diff --git a/tests/imap/imap_002.php b/tests/imap/imap_002.php new file mode 100644 index 0000000000..31f4d10123 --- /dev/null +++ b/tests/imap/imap_002.php @@ -0,0 +1,15 @@ + Date: Mon, 19 Oct 2020 10:51:43 +0200 Subject: [PATCH 4/8] Working without ssl --- src/Peachpie.Library/Mail.cs | 501 +++++++++++++++++++++++++++++++---- 1 file changed, 456 insertions(+), 45 deletions(-) diff --git a/src/Peachpie.Library/Mail.cs b/src/Peachpie.Library/Mail.cs index 8523f07dad..b01cdc1246 100644 --- a/src/Peachpie.Library/Mail.cs +++ b/src/Peachpie.Library/Mail.cs @@ -1,12 +1,16 @@ 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.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -841,13 +845,243 @@ public void SendMessage(string from, string to, string subject, string headers, //[PhpExtension("IMAP")] // uncomment when the extension is ready public static class Imap { - #region Variables - + #region Constants readonly static Encoding ISO_8859_1 = Encoding.GetEncoding("ISO-8859-1"); - #endregion + #region ImapResource + internal abstract class MailResource : PhpResource + { + protected TcpClient _client; + protected SslStream _ssl; + + protected MailResource() : base("imap") {} + + public static MailResource Create(MailBoxInfo info) + { + 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")) + { + var a = GetStream(info); + a.Write(Encoding.ASCII.GetBytes("A\r\n")); + + return ImapResource.Create(info.Hostname, info.Port); + } + else if (info.NameFlags.Contains("pop3")) + { + throw new NotImplementedException(); + } + else if (info.NameFlags.Contains("nntp")) + { + throw new NotImplementedException(); + } + + // Default is imap + return ImapResource.Create(info.Hostname, info.Port); + } + else + { + switch (info.Service) + { + case "pop3": + throw new NotImplementedException(); + case "nntp": + throw new NotImplementedException(); + default: // Default is imap + return ImapResource.Create(info.Hostname, info.Port); + } + } + } + + private static Stream GetStream(MailBoxInfo info) + { + if (info.NameFlags.Contains("ssl")) + { + var client = new TcpClient(info.Hostname, info.Port); + var r = new SslStream(client.GetStream(), false, (sender, cert, chain, sslPolicyErrors) => true); + r.AuthenticateAsClient(info.Hostname); + return r; + } + return null; + + } + + public abstract bool Login(string username, string password); + } + + /// + /// Context of POP3 session. + /// + internal class POP3Resource : MailResource + { + public static POP3Resource Create(string hostname, int port) + { + throw new NotImplementedException(); + } + + public override bool Login(string username, string password) + { + throw new NotImplementedException(); + } + } + + /// + /// Context of NNTP session. + /// + internal class NNTPResource : MailResource + { + public override bool Login(string username, string password) + { + throw new NotImplementedException(); + } + } + + /// + /// Context of IMAP session. + /// + internal class ImapResource : MailResource + { + private enum Status { OK, NO, BAD, None }; + struct ImapResponse + { + public string Tag { get; set; } + public Status Status { get; set; } + public string Body { get; set; } + public byte[] Raw { get; set; } + } + + #region Constants + const char UnTaggedTag = '*'; + const char ContinousTag = '+'; + const char TagPrefix = 'A'; + #endregion + + #region Props + private int _tag = 0; + #endregion + + #region Constructors + private ImapResource() {} + + public static ImapResource Create(string hostname, int port) + { + ImapResource result = new ImapResource(); + result._client = new TcpClient(hostname, port); + + ImapResponse handshake = result.Receive(); + + return result; + } + #endregion + + #region Methods + private void Write(string command) + { + _client.Client.Send(Encoding.ASCII.GetBytes(command)); + } + + private ImapResponse Receive() + { + byte[] buffer = new byte[2]; + int length = _client.Client.Receive(buffer); + + //Wait for complete message. + while (buffer[buffer.Length - 2] != '\r' || buffer[buffer.Length - 1] != '\n') + { + Task.Delay(1); + + if (_client.Available != 0) + { + int size = _client.Available; + byte[] newBuffer = new byte[buffer.Length + size]; + _client.Client.Receive(newBuffer, buffer.Length, size, SocketFlags.None); + Buffer.BlockCopy(buffer, 0, newBuffer, 0, buffer.Length); + buffer = newBuffer; + } + } + + ImapResponse response = new ImapResponse(); + + int index = 0; + + //Tag + 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 < length && buffer[index] >= '0' && buffer[index] <= '0') + index++; + + response.Tag = Encoding.ASCII.GetString(buffer, 0, index); + } + + if (index < buffer.Length && buffer[index] == ' ') + index++; + + //Status + 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 (buffer.Length + 2 < length && buffer[index] == 'D' && buffer[index + 1] == 'A' && buffer[index + 1] == 'D') + { + response.Status = Status.BAD; + index += 3; + } + else + response.Status = Status.None; + } + + //Body + response.Body = Encoding.ASCII.GetString(buffer, index, buffer.Length - index); + response.Raw = buffer.Slice(0, buffer.Length); + + return response; + } + + public override bool Login(string username, string password) + { + string messageTag = $"{TagPrefix}{_tag.ToString()}"; + _tag++; + + Write($"{messageTag} LOGIN {username} {password}\r\n"); + ImapResponse response = Receive(); + if (response.Tag == messageTag) + { + if (response.Status == Status.OK) + return true; + else + return false; + } + else + { + // TODO: Corner case + return false; + } + } + #endregion + } + #endregion /// /// Parses an address string. @@ -883,6 +1117,7 @@ public static PhpArray imap_rfc822_parse_adrlist(string addresses, string defaul #region encode,decode + #region utf7 /// /// Transforms bytes to modified UTF-7 text as defined in RFC 2060 /// @@ -932,8 +1167,8 @@ private static string TransformUTF7ModifiedToUTF8(Context ctx, string text) { if (text[i] == '&') { - if (i == text.Length - 1) - ; // Error + //if (i == text.Length - 1) + // ; // Error if (text[++i] == '-') // Means "&" char. builder.Append("&"); @@ -964,14 +1199,17 @@ private static string TransformUTF7ModifiedToUTF8(Context ctx, string text) } /// - /// Converts UTF16LE encoding to UTF-7 modified(used in IMAP) see RFC 2060. + /// Converts ctx.StringEncoding encoding to UTF-7 modified(used in IMAP) see RFC 2060. /// - private static PhpString UTF16LEToUTF7Modified(Context ctx, string text) + private static PhpString ToUTF7Modified(Context ctx, string text) { if (string.IsNullOrEmpty(text)) return PhpString.Empty; - var builder = StringBuilderUtilities.Pool.Get(); + 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++) { @@ -979,9 +1217,9 @@ private static PhpString UTF16LEToUTF7Modified(Context ctx, string text) if (text[i] >= 0x20 && text[i] <= 0x7e) { if (text[i] == 0x26) - builder.Append("&-"); + writer.Write(ampSequence); else - builder.Append(text[i]); + writer.Write(text[i]); } else // Collects all bytes until Char from 0x20 to 0x7e is reached. { @@ -989,78 +1227,77 @@ private static PhpString UTF16LEToUTF7Modified(Context ctx, string text) while (i < text.Length && (text[i] < 0x20 || text[i] > 0x7e)) i++; - byte[] utf16BE = ctx.StringEncoding.GetBytes(text.ToCharArray(), start, i - start); // Contradiction with RFC. - // byte[] utf16BE = Encoding.BigEndianUnicode.GetBytes(text.ToCharArray(), start, i - start); - string base64Modified = System.Convert.ToBase64String(utf16BE).Replace('/', ',').Trim('='); + 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); - builder.Append("&" + base64Modified + "-"); + string base64Modified = System.Convert.ToBase64String(sequenceEncoded).Replace('/', ',').Trim('='); + + writer.Write('&'); + writer.Write(Encoding.ASCII.GetBytes(base64Modified)); + writer.Write('-'); if (i < text.Length) i--; } } - return new PhpString(StringBuilderUtilities.GetStringAndReturn(builder),ctx); + writer.Flush(); + return new PhpString(stream.ToArray()); } /// - /// Converts UTF-7 modified(used in IMAP) see RFC 2060 encoding to UTF16LE. + /// Converts UTF-7 modified(used in IMAP) see RFC 2060 encoding to . /// - private static PhpString UTF7ModifiedToUTF16LE(Context ctx, PhpString text) + private static PhpString FromUTF7Modified(Context ctx, PhpString text) { if (text.IsEmpty) return string.Empty; byte[] utf7Modified = text.ToBytes(ctx.StringEncoding); - var builder = StringBuilderUtilities.Pool.Get(); + + 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 Exception(); // Error + throw new FormatException(); // Error if (utf7Modified[++i] == '-') // Means "&" char. { - builder.Append("&"); + writer.Write((byte)'&'); } else // Shifting - { - var sectionBuilder = StringBuilderUtilities.Pool.Get(); + { + int start = i; while (i < utf7Modified.Length && utf7Modified[i] != '-') - sectionBuilder.Append((char)utf7Modified[i++]); - - //int start = i; - //while (i < utf7Modified.Length && utf7Modified[i] != '-') - // i++; - //string base64Modified = utf7Modified.Substring(start, i - start); - - string base64Modified = StringBuilderUtilities.GetStringAndReturn(sectionBuilder); + i++; - if ((base64Modified.Length % 4) != 0) // Adds padding - base64Modified = base64Modified.PadRight(base64Modified.Length + 4 - (base64Modified.Length % 4),'='); + string sequence = Encoding.ASCII.GetString(utf7Modified, start, i - start).Replace(',','/'); - base64Modified = base64Modified.Replace(',', '/'); // Replace , + if ((sequence.Length % 4) != 0) // Adds padding + sequence = sequence.PadRight(sequence.Length + 4 - (sequence.Length % 4),'='); - //Decode Base64 and UTF16BE - var a = System.Convert.FromBase64String(base64Modified); + byte[] base64Decoded = System.Convert.FromBase64String(sequence); - builder.Append(ctx.StringEncoding.GetString(System.Convert.FromBase64String(base64Modified))); - //builder.Append(Encoding.BigEndianUnicode.GetString(System.Convert.FromBase64String(base64Modified))); + writer.Write(base64Decoded); } } else if (text[i] >= 0x20 && text[i] <= 0x7e) { - builder.Append(text[i]); + writer.Write((byte)text[i]); } else { - throw new Exception(); // Error + throw new FormatException(); // Error } } - var b = new PhpString(StringBuilderUtilities.GetStringAndReturn(builder),ctx); - return b; + + writer.Flush(); + return new PhpString(stream.ToArray()); } /// @@ -1071,19 +1308,33 @@ private static PhpString UTF7ModifiedToUTF16LE(Context ctx, PhpString text) /// 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 UTF16LEToUTF7Modified(ctx, data.ToString(ctx)); + + + + + + + + + + + + + + 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 UTF7ModifiedToUTF16LE(ctx, text); + return FromUTF7Modified(ctx, text); } #endregion @@ -1099,8 +1350,7 @@ public static string imap_base64(Context ctx, string text) { try { - - return Encoding.Unicode.GetString(Base64Utils.FromBase64(text.AsSpan(), true)); + return ctx.StringEncoding.GetString(Base64Utils.FromBase64(text.AsSpan(), true)); } catch (FormatException) { @@ -1108,8 +1358,169 @@ public static string imap_base64(Context ctx, string text) return string.Empty; } } + + #endregion + #endregion + #region connection, errors, quotas + + 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. + private 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) + { + //TODO: options, n_retires, params + + if (!TryParseHostName(mailbox, out MailBoxInfo info)) + { + return null; + } + + try + { + MailResource resource = MailResource.Create(info); + + if (resource == null) + return null; + if (!resource.Login(username, password)) + return null; + + return resource; + } + catch (SocketException) + { + return null; + } + } #endregion } } From 629c463dd50e36638a3aa4249ba7a12f4aba4b5b Mon Sep 17 00:00:00 2001 From: Tomas Husak Date: Mon, 19 Oct 2020 16:58:02 +0200 Subject: [PATCH 5/8] imap_open --- src/Peachpie.Library/Mail.cs | 269 +++++++++++++++++++++++++++-------- 1 file changed, 211 insertions(+), 58 deletions(-) diff --git a/src/Peachpie.Library/Mail.cs b/src/Peachpie.Library/Mail.cs index b01cdc1246..951e285317 100644 --- a/src/Peachpie.Library/Mail.cs +++ b/src/Peachpie.Library/Mail.cs @@ -847,68 +847,115 @@ 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 { - protected TcpClient _client; - protected SslStream _ssl; + private enum Service { IMAP, NNTP, POP3}; + + protected Stream _stream; - protected MailResource() : base("imap") {} + #region Contructors + protected MailResource() : base("imap") { } - public static MailResource Create(MailBoxInfo info) + public static MailResource Create(MailBoxInfo info) { 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")) { - var a = GetStream(info); - a.Write(Encoding.ASCII.GetBytes("A\r\n")); - - return ImapResource.Create(info.Hostname, info.Port); + return CreateImap(info, GetStream(info)); } else if (info.NameFlags.Contains("pop3")) { - throw new NotImplementedException(); + return CreatePop3(info, GetStream(info)); } else if (info.NameFlags.Contains("nntp")) { - throw new NotImplementedException(); + return CreateNntp(info, GetStream(info)); } // Default is imap - return ImapResource.Create(info.Hostname, info.Port); + return CreateImap(info, GetStream(info)); } else { switch (info.Service) { case "pop3": - throw new NotImplementedException(); + return CreatePop3(info, GetStream(info)); case "nntp": - throw new NotImplementedException(); + return CreateNntp(info, GetStream(info)); default: // Default is imap - return ImapResource.Create(info.Hostname, info.Port); + return CreateImap(info, GetStream(info)); } } } + private static ImapResource CreateImap(MailBoxInfo info, Stream stream) + { + ImapResource resource = ImapResource.Create(info, GetStream(info)); + + if (info.NameFlags.Contains("secure")) // StartTLS with validation + { + resource.StartTLS(true, info); + } + + if (info.NameFlags.Contains("tls"))// StartTLS + { + resource.StartTLS(!info.NameFlags.Contains("novalidate-cert"), info); + } + + return resource; + } + + private static ImapResource CreatePop3(MailBoxInfo info, Stream stream) + { + throw new NotImplementedException(); + } + + private static ImapResource CreateNntp(MailBoxInfo info, Stream stream) + { + throw new NotImplementedException(); + } + private static Stream GetStream(MailBoxInfo info) { + TcpClient client = new TcpClient(info.Hostname, info.Port); + + if (info.NameFlags.Contains("notls")) + return client.GetStream(); + if (info.NameFlags.Contains("ssl")) - { - var client = new TcpClient(info.Hostname, info.Port); - var r = new SslStream(client.GetStream(), false, (sender, cert, chain, sslPolicyErrors) => true); - r.AuthenticateAsClient(info.Hostname); - return r; + { + if (info.NameFlags.Contains("novalidate-cert")) + { + SslStream stream = new SslStream(client.GetStream(), false, (sender, certificate, chain, sslPolicyErrors) => true); + stream.AuthenticateAsClient(info.Hostname); + return stream; + } + else // Validate Certificate + { + throw new NotImplementedException(); + } } - return null; + return client.GetStream(); } + #endregion + #region Methods public abstract bool Login(string username, string password); + + public abstract void Close(); + #endregion } /// @@ -921,6 +968,11 @@ public static POP3Resource Create(string hostname, int port) throw new NotImplementedException(); } + public override void Close() + { + throw new NotImplementedException(); + } + public override bool Login(string username, string password) { throw new NotImplementedException(); @@ -932,6 +984,11 @@ public override bool Login(string username, string password) /// internal class NNTPResource : MailResource { + public override void Close() + { + throw new NotImplementedException(); + } + public override bool Login(string username, string password) { throw new NotImplementedException(); @@ -944,7 +1001,7 @@ public override bool Login(string username, string password) internal class ImapResource : MailResource { private enum Status { OK, NO, BAD, None }; - struct ImapResponse + class ImapResponse { public string Tag { get; set; } public Status Status { get; set; } @@ -963,49 +1020,96 @@ struct ImapResponse #endregion #region Constructors - private ImapResource() {} + private ImapResource() { } - public static ImapResource Create(string hostname, int port) + public static ImapResource Create(MailBoxInfo info, Stream stream) { - ImapResource result = new ImapResource(); - result._client = new TcpClient(hostname, port); + ImapResource resource = new ImapResource(); + resource._stream = stream; - ImapResponse handshake = result.Receive(); + ImapResponse response = resource.Receive(); - return result; + return (response.Status == Status.OK) ? resource : null; } #endregion #region Methods + public bool StartTLS(bool sslValidation, MailBoxInfo info) + { + string messageTag = $"{TagPrefix}{_tag.ToString()}"; + string command = $"{messageTag} STARTTLS\r\n"; + + Write(command); + + ImapResponse response = Receive(); + while (response.Tag != messageTag) + response = Receive(); + + if (response.Status != Status.OK) + return false; + + if (sslValidation) + { + throw new NotImplementedException(); + } + else + { + SslStream stream = new SslStream(_stream, false, (sender, certificate, chain, sslPolicyErrors) => true); + stream.AuthenticateAsClient(info.Hostname); + _stream = stream; + return true; + } + } + private void Write(string command) { - _client.Client.Send(Encoding.ASCII.GetBytes(command)); + _stream.Write(Encoding.ASCII.GetBytes(command)); + _tag++; } - private ImapResponse Receive() + private ImapResponse Receive(bool wait = true) { byte[] buffer = new byte[2]; - int length = _client.Client.Receive(buffer); + 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[buffer.Length - 2] != '\r' || buffer[buffer.Length - 1] != '\n') + while (buffer[length - 2] != '\r' || buffer[length - 1] != '\n') { Task.Delay(1); + int bufferSize = 1024; - if (_client.Available != 0) - { - int size = _client.Available; - byte[] newBuffer = new byte[buffer.Length + size]; - _client.Client.Receive(newBuffer, buffer.Length, size, SocketFlags.None); - Buffer.BlockCopy(buffer, 0, newBuffer, 0, buffer.Length); - buffer = newBuffer; - } + 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; } ImapResponse response = new ImapResponse(); int index = 0; - + //Tag if (buffer[index] == UnTaggedTag) { @@ -1052,8 +1156,8 @@ private ImapResponse Receive() } //Body - response.Body = Encoding.ASCII.GetString(buffer, index, buffer.Length - index); - response.Raw = buffer.Slice(0, buffer.Length); + response.Body = Encoding.ASCII.GetString(buffer, index, length - index); + response.Raw = buffer.Slice(0, length); return response; } @@ -1061,28 +1165,48 @@ private ImapResponse Receive() public override bool Login(string username, string password) { string messageTag = $"{TagPrefix}{_tag.ToString()}"; - _tag++; Write($"{messageTag} LOGIN {username} {password}\r\n"); ImapResponse response = Receive(); - if (response.Tag == messageTag) + while (response.Tag != messageTag) { - if (response.Status == Status.OK) - return true; - else - return false; - } - else - { - // TODO: Corner case - return false; + response = Receive(); } + + return response.Status == Status.OK; + } + + + public override void Close() => FreeManaged(); + + 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. /// @@ -1114,6 +1238,7 @@ public static PhpArray imap_rfc822_parse_adrlist(string addresses, string defaul // return arr; } + #endregion #region encode,decode @@ -1382,7 +1507,7 @@ internal class MailBoxInfo /// The mailbox has the format: "{" remote_system_name [":" port] [flags] "}" [mailbox_name] /// Parsed information about mailbox. /// True on Success, False on failure. - private static bool TryParseHostName(string mailbox, out MailBoxInfo info) + static bool TryParseHostName(string mailbox, out MailBoxInfo info) { info = new MailBoxInfo(); if (String.IsNullOrEmpty(mailbox)) @@ -1498,12 +1623,18 @@ string GetName(string mailbox) [return: CastToFalse] public static PhpResource imap_open(string mailbox, string username , string password, int options, int n_retries, PhpArray @params) { - //TODO: options, n_retires, params - + // Unsupported flags: authuser, debug, (nntp, pop3 - can be done), (validate-cert - maybe 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 { @@ -1521,6 +1652,28 @@ public static PhpResource imap_open(string mailbox, string username , string pas 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 } } From 0c53a90be9ebabbae0fdcef385b229ed5a8a0bbe Mon Sep 17 00:00:00 2001 From: Tomas Husak Date: Mon, 19 Oct 2020 22:07:03 +0200 Subject: [PATCH 6/8] Certificate Validation --- src/Peachpie.Library/Mail.cs | 232 ++++++++++++++++++++++------------- 1 file changed, 150 insertions(+), 82 deletions(-) diff --git a/src/Peachpie.Library/Mail.cs b/src/Peachpie.Library/Mail.cs index e31accba28..856dc15bc7 100644 --- a/src/Peachpie.Library/Mail.cs +++ b/src/Peachpie.Library/Mail.cs @@ -11,6 +11,7 @@ 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; @@ -934,26 +935,37 @@ private static Stream GetStream(MailBoxInfo info) return client.GetStream(); if (info.NameFlags.Contains("ssl")) - { + { + SslStream stream; if (info.NameFlags.Contains("novalidate-cert")) - { - SslStream stream = new SslStream(client.GetStream(), false, (sender, certificate, chain, sslPolicyErrors) => true); - stream.AuthenticateAsClient(info.Hostname); - return stream; - } + stream = new SslStream(client.GetStream(), false, (sender, certificate, chain, sslPolicyErrors) => true); else // Validate Certificate - { - throw new NotImplementedException(); - } + stream = new SslStream(client.GetStream(), false, ValidateServerCertificate); + + stream.AuthenticateAsClient(info.Hostname); + return stream; } return client.GetStream(); } + + protected static bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + { + if (sslPolicyErrors == SslPolicyErrors.None) + { + return true; + } + + Console.WriteLine("Certificate error: {0}", sslPolicyErrors); + + // refuse connection + return false; + } #endregion #region Methods public abstract bool Login(string username, string password); - + public abstract bool Select(string path); public abstract void Close(); #endregion } @@ -977,6 +989,11 @@ public override bool Login(string username, string password) { throw new NotImplementedException(); } + + public override bool Select(string path) + { + throw new NotImplementedException(); + } } /// @@ -993,6 +1010,11 @@ public override bool Login(string username, string password) { throw new NotImplementedException(); } + + public override bool Select(string path) + { + throw new NotImplementedException(); + } } /// @@ -1007,6 +1029,64 @@ class ImapResponse public Status Status { get; set; } public string Body { get; set; } public byte[] Raw { get; set; } + + public static bool TryParse(byte[] buffer, out ImapResponse response) + { + response = new ImapResponse(); + + int index = 0; + + //Tag + 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 + 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 + response.Body = Encoding.ASCII.GetString(buffer, index, buffer.Length - index); + response.Raw = buffer.Slice(0, buffer.Length); + + return true; + } } #region Constants @@ -1027,9 +1107,9 @@ public static ImapResource Create(MailBoxInfo info, Stream stream) ImapResource resource = new ImapResource(); resource._stream = stream; - ImapResponse response = resource.Receive(); + List responses = resource.Receive(); - return (response.Status == Status.OK) ? resource : null; + return (responses[0].Status == Status.OK) ? resource : null; } #endregion @@ -1041,24 +1121,27 @@ public bool StartTLS(bool sslValidation, MailBoxInfo info) Write(command); - ImapResponse response = Receive(); - while (response.Tag != messageTag) - response = Receive(); - - if (response.Status != Status.OK) - return false; - - if (sslValidation) + bool completed = false; + while (!completed) { - throw new NotImplementedException(); + List responses = Receive(); + foreach (var response in responses) + if (response.Tag == messageTag) + if (response.Status != Status.OK) + return false; + else + completed = true; } + + SslStream stream; + if (sslValidation) + stream = new SslStream(_stream, false, ValidateServerCertificate); else - { - SslStream stream = new SslStream(_stream, false, (sender, certificate, chain, sslPolicyErrors) => true); - stream.AuthenticateAsClient(info.Hostname); - _stream = stream; - return true; - } + stream = new SslStream(_stream, false, (sender, certificate, chain, sslPolicyErrors) => true); + + stream.AuthenticateAsClient(info.Hostname); + _stream = stream; + return true; } private void Write(string command) @@ -1067,7 +1150,7 @@ private void Write(string command) _tag++; } - private ImapResponse Receive(bool wait = true) + private List Receive(bool wait = true) { byte[] buffer = new byte[2]; int length = 0; @@ -1106,60 +1189,22 @@ private ImapResponse Receive(bool wait = true) buffer = newBuffer; } - ImapResponse response = new ImapResponse(); - - int index = 0; - - //Tag - 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 < length && buffer[index] >= '0' && buffer[index] <= '0') - index++; - - response.Tag = Encoding.ASCII.GetString(buffer, 0, index); - } - - if (index < buffer.Length && buffer[index] == ' ') - index++; - - //Status - if (index + 1 < buffer.Length) + List responses = new List(); + int startIndex = 0; + for (int i = 1; i < buffer.Length; i++) { - 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 (buffer.Length + 2 < length && buffer[index] == 'D' && buffer[index + 1] == 'A' && buffer[index + 1] == 'D') + if (buffer[i] == '\n' && buffer[i - 1] == '\r') // Split { - response.Status = Status.BAD; - index += 3; + if (!ImapResponse.TryParse(buffer.Slice(startIndex, i - startIndex + 1), out ImapResponse imap)) + return null; + else + responses.Add(imap); + + startIndex = i + 1; } - else - response.Status = Status.None; } - //Body - response.Body = Encoding.ASCII.GetString(buffer, index, length - index); - response.Raw = buffer.Slice(0, length); - - return response; + return responses; } public override bool Login(string username, string password) @@ -1168,15 +1213,29 @@ public override bool Login(string username, string password) Write($"{messageTag} LOGIN {username} {password}\r\n"); - ImapResponse response = Receive(); - while (response.Tag != messageTag) + while (true) { - response = Receive(); + List responses = Receive(); + foreach (var response in responses) + if (response.Tag == messageTag) + return response.Status == Status.OK; } - - return response.Status == Status.OK; } + public override bool Select(string path) + { + string messageTag = $"{TagPrefix}{_tag.ToString()}"; + + Write($"{messageTag} SELECT {path}\r\n"); + + while (true) + { + List responses = Receive(); + foreach (var response in responses) + if (response.Tag == messageTag) + return response.Status == Status.OK; + } + } public override void Close() => FreeManaged(); @@ -1644,6 +1703,15 @@ public static PhpResource imap_open(string mailbox, string username , string pas return null; if (!resource.Login(username, password)) return null; + + if (String.IsNullOrEmpty(info.MailBoxName)) + { + resource.Select("INBOX"); + } + else + { + resource.Select(info.MailBoxName); + } return resource; } From c6c77dcf3ec17a4f262c08aea93cf35496c09b4c Mon Sep 17 00:00:00 2001 From: Tomas Husak Date: Mon, 26 Oct 2020 22:16:47 +0100 Subject: [PATCH 7/8] POP3 support for imap_open --- src/Peachpie.Library/Mail.cs | 586 +++++++++++++++++++++++++++-------- 1 file changed, 462 insertions(+), 124 deletions(-) diff --git a/src/Peachpie.Library/Mail.cs b/src/Peachpie.Library/Mail.cs index 856dc15bc7..43fd0e5efd 100644 --- a/src/Peachpie.Library/Mail.cs +++ b/src/Peachpie.Library/Mail.cs @@ -860,110 +860,247 @@ 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; + 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")) - { - return CreateImap(info, GetStream(info)); - } + result = CreateImap(info, GetStream(info)); else if (info.NameFlags.Contains("pop3")) - { - return CreatePop3(info, GetStream(info)); - } + result = CreatePop3(info, GetStream(info)); else if (info.NameFlags.Contains("nntp")) - { - return CreateNntp(info, GetStream(info)); - } - - // Default is imap - return CreateImap(info, GetStream(info)); + result = CreateNntp(info, GetStream(info)); + else // Default is imap + result = CreateImap(info, GetStream(info)); } else { switch (info.Service) { case "pop3": - return CreatePop3(info, GetStream(info)); + result = CreatePop3(info, GetStream(info)); + break; case "nntp": - return CreateNntp(info, GetStream(info)); + result = CreateNntp(info, GetStream(info)); + break; default: // Default is imap - return CreateImap(info, GetStream(info)); + result = CreateImap(info, GetStream(info)); + 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) { - ImapResource resource = ImapResource.Create(info, GetStream(info)); + if (info == null || stream == null) + return null; - if (info.NameFlags.Contains("secure")) // StartTLS with validation - { - resource.StartTLS(true, info); - } + ImapResource resource = ImapResource.Create(GetStream(info)); + if (resource == null) + return null; - if (info.NameFlags.Contains("tls"))// StartTLS - { - resource.StartTLS(!info.NameFlags.Contains("novalidate-cert"), info); - } + 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; } - private static ImapResource CreatePop3(MailBoxInfo info, Stream stream) + /// + /// 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) { - throw new NotImplementedException(); + 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) { - TcpClient client = new TcpClient(info.Hostname, info.Port); + 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")) + 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 { - SslStream stream; if (info.NameFlags.Contains("novalidate-cert")) - stream = new SslStream(client.GetStream(), false, (sender, certificate, chain, sslPolicyErrors) => true); + result = new SslStream(stream, false, (sender, certificate, chain, sslPolicyErrors) => true); else // Validate Certificate - stream = new SslStream(client.GetStream(), false, ValidateServerCertificate); + result = new SslStream(stream, false, ValidateServerCertificate); - stream.AuthenticateAsClient(info.Hostname); - return stream; + result.AuthenticateAsClient(info.Hostname); + } + catch (AuthenticationException) + { + return null; } - return client.GetStream(); + 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; - } - Console.WriteLine("Certificate error: {0}", sslPolicyErrors); - - // refuse connection + // 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 Select(string path); public abstract void Close(); @@ -975,25 +1112,184 @@ protected static bool ValidateServerCertificate(object sender, X509Certificate c /// internal class POP3Resource : MailResource { - public static POP3Resource Create(string hostname, int port) + /// + /// 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 { - throw new NotImplementedException(); + /// + /// 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; + } } - public override void Close() + #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) { - throw new NotImplementedException(); + 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) { - throw new NotImplementedException(); + 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); } - public override bool Select(string path) + public override bool Select(string path) => throw new NotImplementedException(); + + public override void Close() => FreeManaged(); + + protected override void FreeManaged() { - throw new NotImplementedException(); + _stream.Close(); + _stream.Dispose(); + base.FreeManaged(); } + #endregion } /// @@ -1015,6 +1311,11 @@ public override bool Select(string path) { throw new NotImplementedException(); } + + protected override bool StartTLS(bool sslValidation) + { + throw new NotImplementedException(); + } } /// @@ -1022,21 +1323,48 @@ public override bool Select(string path) /// 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 { - public string Tag { get; set; } - public Status Status { get; set; } - public string Body { get; set; } - public byte[] Raw { get; set; } - + /// + /// 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 + // Tag property if (buffer[index] == UnTaggedTag) { response.Tag = UnTaggedTag.ToString(); @@ -1059,7 +1387,7 @@ public static bool TryParse(byte[] buffer, out ImapResponse response) if (index < buffer.Length && buffer[index] == ' ') index++; - //Status + // Status property if (index + 1 < buffer.Length) { if (buffer[index] == 'N' && buffer[index + 1] == 'O') @@ -1081,123 +1409,124 @@ public static bool TryParse(byte[] buffer, out ImapResponse response) response.Status = Status.None; } - //Body + // Body property response.Body = Encoding.ASCII.GetString(buffer, index, buffer.Length - index); - response.Raw = buffer.Slice(0, buffer.Length); + + // 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 Props + #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() { } + private ImapResource() {} - public static ImapResource Create(MailBoxInfo info, Stream stream) + /// + /// 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 - public bool StartTLS(bool sslValidation, MailBoxInfo info) + + /// + /// 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) + 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) - if (response.Status != Status.OK) + 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; } - SslStream stream; - if (sslValidation) - stream = new SslStream(_stream, false, ValidateServerCertificate); + var stream = MakeSslConection(_stream, _info); + if (stream == null) + { + return false; + } else - stream = new SslStream(_stream, false, (sender, certificate, chain, sslPolicyErrors) => true); - - stream.AuthenticateAsClient(info.Hostname); - _stream = stream; - return true; + { + _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 = 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; - } + 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 + 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)) - return null; - else + if (ImapResponse.TryParse(buffer.Slice(startIndex, i - startIndex + 1), out ImapResponse imap)) responses.Add(imap); startIndex = i + 1; @@ -1207,13 +1536,16 @@ private List Receive(bool wait = true) 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) + while (true) // Waits for the response. { List responses = Receive(); foreach (var response in responses) @@ -1222,13 +1554,17 @@ public override bool Login(string username, string password) } } + /// + /// Excutes command SELECT {path}. + /// + /// The path in mailbox. + /// Returns true on success or false on failure. public override bool Select(string path) { string messageTag = $"{TagPrefix}{_tag.ToString()}"; - Write($"{messageTag} SELECT {path}\r\n"); - while (true) + while (true) // Waits for the response. { List responses = Receive(); foreach (var response in responses) @@ -1549,6 +1885,9 @@ public static string imap_base64(Context ctx, string text) #region connection, errors, quotas + /// + /// Represents parsed "conection string" from image_open. + /// internal class MailBoxInfo { public string Hostname { get; set; } @@ -1682,7 +2021,7 @@ string GetName(string mailbox) [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, pop3 - can be done), (validate-cert - maybe can be done), readonly + // 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 @@ -1703,16 +2042,15 @@ public static PhpResource imap_open(string mailbox, string username , string pas return null; if (!resource.Login(username, password)) return null; - - if (String.IsNullOrEmpty(info.MailBoxName)) - { - resource.Select("INBOX"); - } - else + + if (resource is ImapResource imap) // There is only one folder in POP3. { - resource.Select(info.MailBoxName); + if (String.IsNullOrEmpty(info.MailBoxName)) + imap.Select("INBOX"); + else + imap.Select(info.MailBoxName); } - + return resource; } catch (SocketException) From 2e4a9628e59d1a3ceed80ee46247c4d1960df795 Mon Sep 17 00:00:00 2001 From: Tomas Husak Date: Mon, 26 Oct 2020 22:51:49 +0100 Subject: [PATCH 8/8] imap_close support for POP3 IMAP --- src/Peachpie.Library/Mail.cs | 74 +++++++++++++++++++++++++++--------- 1 file changed, 55 insertions(+), 19 deletions(-) diff --git a/src/Peachpie.Library/Mail.cs b/src/Peachpie.Library/Mail.cs index 43fd0e5efd..54b6ecd647 100644 --- a/src/Peachpie.Library/Mail.cs +++ b/src/Peachpie.Library/Mail.cs @@ -883,31 +883,35 @@ public static MailResource Create(MailBoxInfo info) 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, GetStream(info)); + result = CreateImap(info, stream); else if (info.NameFlags.Contains("pop3")) - result = CreatePop3(info, GetStream(info)); + result = CreatePop3(info, stream); else if (info.NameFlags.Contains("nntp")) - result = CreateNntp(info, GetStream(info)); + result = CreateNntp(info, stream); else // Default is imap - result = CreateImap(info, GetStream(info)); + result = CreateImap(info, stream); } else { + switch (info.Service) { case "pop3": - result = CreatePop3(info, GetStream(info)); + result = CreatePop3(info, stream); break; case "nntp": - result = CreateNntp(info, GetStream(info)); + result = CreateNntp(info, stream); break; default: // Default is imap - result = CreateImap(info, GetStream(info)); + result = CreateImap(info, stream); break; } } @@ -1102,8 +1106,7 @@ protected byte[] ReceiveBytes(bool wait = true) } protected abstract bool StartTLS(bool sslValidation); public abstract bool Login(string username, string password); - public abstract bool Select(string path); - public abstract void Close(); + public abstract bool Close(); #endregion } @@ -1279,9 +1282,21 @@ public override bool Login(string username, string password) return ((responses != null || responses.Count != 0) && responses[0].Status == Status.OK); } - public override bool Select(string path) => throw new NotImplementedException(); + /// + /// 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); - public override void Close() => FreeManaged(); + if (result) + FreeManaged(); + + return result; + } protected override void FreeManaged() { @@ -1297,7 +1312,7 @@ protected override void FreeManaged() /// internal class NNTPResource : MailResource { - public override void Close() + public override bool Close() { throw new NotImplementedException(); } @@ -1307,11 +1322,6 @@ public override bool Login(string username, string password) throw new NotImplementedException(); } - public override bool Select(string path) - { - throw new NotImplementedException(); - } - protected override bool StartTLS(bool sslValidation) { throw new NotImplementedException(); @@ -1559,7 +1569,7 @@ public override bool Login(string username, string password) /// /// The path in mailbox. /// Returns true on success or false on failure. - public override bool Select(string path) + public bool Select(string path) { string messageTag = $"{TagPrefix}{_tag.ToString()}"; Write($"{messageTag} SELECT {path}\r\n"); @@ -1573,7 +1583,33 @@ public override bool Select(string path) } } - public override void Close() => FreeManaged(); + /// + /// 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() {