From a61b14113c3d203a0003d288d7c6daa44bf9b3b8 Mon Sep 17 00:00:00 2001 From: smiley22 Date: Mon, 20 Jan 2014 21:05:46 +0100 Subject: [PATCH] Bugfix IdleLoop addresses issues mentioned in #20, #51 and #61 --- IImapClient.cs | 4 +- IdleErrorEventArgs.cs | 38 +++++++++++ IdleEvents.cs => IdleMessageEventArgs.cs | 0 ImapClient.cs | 85 +++++++++++++----------- Properties/AssemblyInfo.cs | 4 +- S22.Imap.csproj | 8 +-- Tests/SequenceSetTest.cs | 11 +++ Tests/Tests.csproj | 9 ++- Util.cs | 13 ++-- 9 files changed, 115 insertions(+), 57 deletions(-) create mode 100644 IdleErrorEventArgs.cs rename IdleEvents.cs => IdleMessageEventArgs.cs (100%) diff --git a/IImapClient.cs b/IImapClient.cs index ff857cc..b8652cf 100644 --- a/IImapClient.cs +++ b/IImapClient.cs @@ -5,8 +5,8 @@ namespace S22.Imap { /// - /// Enables applications to communicate with a mail server using the - /// Internet Message Access Protocol (IMAP). + /// Enables applications to communicate with a mail server using the Internet Message Access + /// Protocol (IMAP). /// public interface IImapClient : IDisposable { /// diff --git a/IdleErrorEventArgs.cs b/IdleErrorEventArgs.cs new file mode 100644 index 0000000..0bff254 --- /dev/null +++ b/IdleErrorEventArgs.cs @@ -0,0 +1,38 @@ +using System; + +namespace S22.Imap { + /// + /// Provides data for IMAP idle error events. + /// + public class IdleErrorEventArgs : EventArgs { + /// + /// Initializes a new instance of the IdleErrorEventArgs class. + /// + /// The exception that causes the event. + /// The instance of the ImapClient class that raised the event. + /// The exception parameter or the client parameter + /// is null. + internal IdleErrorEventArgs(Exception exception, ImapClient client) { + exception.ThrowIfNull("exception"); + client.ThrowIfNull("client"); + Exception = exception; + Client = client; + } + + /// + /// The exception that caused the error event. + /// + public Exception Exception { + get; + private set; + } + + /// + /// The instance of the ImapClient class that raised the event. + /// + public ImapClient Client { + get; + private set; + } + } +} diff --git a/IdleEvents.cs b/IdleMessageEventArgs.cs similarity index 100% rename from IdleEvents.cs rename to IdleMessageEventArgs.cs diff --git a/ImapClient.cs b/ImapClient.cs index 738ba20..70f1aee 100644 --- a/ImapClient.cs +++ b/ImapClient.cs @@ -16,8 +16,8 @@ namespace S22.Imap { /// - /// Enables applications to communicate with a mail server using the - /// Internet Message Access Protocol (IMAP). + /// Enables applications to communicate with a mail server using the Internet Message Access + /// Protocol (IMAP). /// public class ImapClient : IImapClient { @@ -114,11 +114,19 @@ public event EventHandler MessageDeleted { } } + /// + /// The event that is raised when an I/O exception occurs in the idle-thread. + /// + /// + /// An I/O exception can occur if the underlying network connection has been reset or the + /// server unexpectedly closed the connection. + /// + public event EventHandler IdleError; + /// /// This constructor is solely used for unit testing. /// - /// A stream to initialize the ImapClient instance - /// with. + /// A stream to initialize the ImapClient instance with. internal ImapClient(Stream stream) { this.stream = stream; Authed = true; @@ -402,9 +410,8 @@ string Authenticate(string tag, string username, string password, "mechanism is not supported by the server."); } while (!m.IsCompleted) { - // Annoyingly, Gmail OAUTH2 issues an untagged capability response during - // the SASL authentication process. As per spec this is illegal, but we - // should still deal with it. + // Annoyingly, Gmail OAUTH2 issues an untagged capability response during the SASL + // authentication process. As per spec this is illegal, but we should still deal with it. while (response.StartsWith("*")) response = GetResponse(); // Stop if the server response starts with our tag. @@ -463,8 +470,8 @@ string GetTag() { /// prior to sending. void SendCommand(string command) { ts.TraceInformation("C -> " + command); - // We can safely use UTF-8 here since it's backwards compatible with ASCII - // and comes in handy when sending strings in literal form (see RFC 3501, 4.3). + // We can safely use UTF-8 here since it's backwards compatible with ASCII and comes in handy + // when sending strings in literal form (see RFC 3501, 4.3). byte[] bytes = Encoding.UTF8.GetBytes(command + "\r\n"); lock (writeLock) { stream.Write(bytes, 0, bytes.Length); @@ -502,7 +509,10 @@ string GetResponse(bool resolveLiterals = true) { using (var mem = new MemoryStream()) { lock (readLock) { while (true) { - byte b = (byte)stream.ReadByte(); + int i = stream.ReadByte(); + if (i == -1) + throw new IOException("The stream could not be read."); + byte b = (byte)i; if (b == CarriageReturn) continue; if (b == Newline) { @@ -716,7 +726,6 @@ void SelectMailbox(string mailbox) { string tag = GetTag(); string response = SendCommandGetResponse(tag + "SELECT " + Util.UTF7Encode(mailbox).QuoteString()); - // Fixme: evaluate untagged data? while (response.StartsWith("*")) response = GetResponse(); if (!IsResponseOK(response, tag)) @@ -748,8 +757,7 @@ public IEnumerable ListMailboxes() { string tag = GetTag(); string response = SendCommandGetResponse(tag + "LIST \"\" \"*\""); while (response.StartsWith("*")) { - Match m = Regex.Match(response, - "\\* LIST \\((.*)\\)\\s+\"([^\"]+)\"\\s+(.+)"); + Match m = Regex.Match(response, "\\* LIST \\((.*)\\)\\s+\"([^\"]+)\"\\s+(.+)"); if (m.Success) { string[] attr = m.Groups[1].Value.Split(' '); bool add = true; @@ -758,14 +766,13 @@ public IEnumerable ListMailboxes() { if (a.ToLower() == @"\noselect") add = false; } - // Names _should_ be enclosed in double-quotes but not all servers - // follow through with this, so we don't enforce it in the above regex. + // Names _should_ be enclosed in double-quotes but not all servers follow through with + // this, so we don't enforce it in the above regex. string name = Regex.Replace(m.Groups[3].Value, "^\"(.+)\"$", "$1"); try { name = Util.UTF7Decode(name); } catch { - // Include the unaltered string in the result if UTF-7 decoding - // failed for any reason. + // Include the unaltered string in the result if UTF-7 decoding failed for any reason. } if (add) mailboxes.Add(name); @@ -832,8 +839,7 @@ public void Expunge(string mailbox = null) { /// public MailboxInfo GetMailboxInfo(string mailbox = null) { AssertValid(); - // This is not a cheap method to call, it involves a couple of round-trips - // to the server. + // This is not a cheap method to call, it involves a couple of round-trips to the server. lock (sequenceLock) { PauseIdling(); if (mailbox == null) @@ -1001,7 +1007,7 @@ public IEnumerable Search(SearchCondition criteria, string mailbox = null) if (!response.StartsWith("+")) { ResumeIdling(); throw new NotSupportedException("Please restrict your search " + - "to ASCII-only characters", new BadServerResponseException(response)); + "to ASCII-only characters.", new BadServerResponseException(response)); } response = SendCommandGetResponse(line); } @@ -1732,9 +1738,8 @@ public void DeleteMessages(IEnumerable uids, string mailbox = null) { string tag = GetTag(); string response = SendCommandGetResponse(tag + "UID STORE " + set + @" +FLAGS.SILENT (\Deleted \Seen)"); - while (response.StartsWith("*")) { + while (response.StartsWith("*")) response = GetResponse(); - } ResumeIdling(); if (!IsResponseOK(response, tag)) throw new BadServerResponseException(response); @@ -1777,8 +1782,7 @@ public IEnumerable GetMessageFlags(uint uid, string mailbox = null) PauseIdling(); SelectMailbox(mailbox); string tag = GetTag(); - string response = SendCommandGetResponse(tag + "UID FETCH " + uid + - " (FLAGS)"); + string response = SendCommandGetResponse(tag + "UID FETCH " + uid + " (FLAGS)"); List flags = new List(); while (response.StartsWith("*")) { Match m = Regex.Match(response, @"FLAGS \(([\w\s\\$-]*)\)"); @@ -1832,9 +1836,8 @@ public void SetMessageFlags(uint uid, string mailbox, params MessageFlag[] flags string tag = GetTag(); string response = SendCommandGetResponse(tag + "UID STORE " + uid + @" FLAGS.SILENT (" + flagsString.Trim() + ")"); - while (response.StartsWith("*")) { + while (response.StartsWith("*")) response = GetResponse(); - } ResumeIdling(); if (!IsResponseOK(response, tag)) throw new BadServerResponseException(response); @@ -1875,9 +1878,8 @@ public void AddMessageFlags(uint uid, string mailbox, params MessageFlag[] flags string tag = GetTag(); string response = SendCommandGetResponse(tag + "UID STORE " + uid + @" +FLAGS.SILENT (" + flagsString.Trim() + ")"); - while (response.StartsWith("*")) { + while (response.StartsWith("*")) response = GetResponse(); - } ResumeIdling(); if (!IsResponseOK(response, tag)) throw new BadServerResponseException(response); @@ -1918,9 +1920,8 @@ public void RemoveMessageFlags(uint uid, string mailbox, params MessageFlag[] fl string tag = GetTag(); string response = SendCommandGetResponse(tag + "UID STORE " + uid + @" -FLAGS.SILENT (" + flagsString.Trim() + ")"); - while (response.StartsWith("*")) { + while (response.StartsWith("*")) response = GetResponse(); - } ResumeIdling(); if (!IsResponseOK(response, tag)) throw new BadServerResponseException(response); @@ -1950,8 +1951,7 @@ void StartIdling() { if (idling) return; if (!Supports("IDLE")) - throw new InvalidOperationException("The server does not support the " + - "IMAP4 IDLE command"); + throw new InvalidOperationException("The server does not support the IMAP4 IDLE command."); lock (sequenceLock) { // Make sure the default mailbox is selected. SelectMailbox(null); @@ -1963,7 +1963,7 @@ void StartIdling() { } // Setup and start the idle thread. if (idleThread != null) - throw new ApplicationException("idleThread is not null"); + throw new ApplicationException("idleThread is not null."); idling = true; idleThread = new Thread(IdleLoop); idleThread.IsBackground = true; @@ -2069,7 +2069,7 @@ void ResumeIdling() { } // Setup and start the idle thread. if (idleThread != null) - throw new ApplicationException("idleThread is not null"); + throw new ApplicationException("idleThread is not null."); idleThread = new Thread(IdleLoop); idleThread.IsBackground = true; idleThread.Start(); @@ -2112,7 +2112,19 @@ void IdleLoop() { return; // Otherwise we should let it bubble up. // FIXME: Raise an error event? - throw; + // Shutdown idleThread. + // Stop Timer. + // Set idling to false. + idleThread = null; + idling = false; + noopTimer.Stop(); + try { + IdleError.Raise(this, new IdleErrorEventArgs(e, this)); + } catch { + } + Console.WriteLine("Shutting down IdleLoop"); + return; + } } } @@ -2198,8 +2210,7 @@ IEnumerable GetQuota(string mailbox = null) { string response = SendCommandGetResponse(tag + "GETQUOTAROOT " + Util.UTF7Encode(mailbox).QuoteString()); while (response.StartsWith("*")) { - Match m = Regex.Match(response, - "\\* QUOTA \"(\\w*)\" \\((\\w+)\\s+(\\d+)\\s+(\\d+)\\)"); + Match m = Regex.Match(response, "\\* QUOTA \"(\\w*)\" \\((\\w+)\\s+(\\d+)\\s+(\\d+)\\)"); if (m.Success) { try { MailboxQuota quota = new MailboxQuota(m.Groups[2].Value, diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index 1cc1936..b742ca0 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -35,5 +35,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("3.4.0.3")] -[assembly: AssemblyFileVersion("3.4.0.3")] +[assembly: AssemblyVersion("3.5.0.0")] +[assembly: AssemblyFileVersion("3.5.0.0")] diff --git a/S22.Imap.csproj b/S22.Imap.csproj index 0b5b537..77f6064 100644 --- a/S22.Imap.csproj +++ b/S22.Imap.csproj @@ -69,7 +69,8 @@ - + + @@ -125,9 +126,6 @@ --> - + \ No newline at end of file diff --git a/Tests/SequenceSetTest.cs b/Tests/SequenceSetTest.cs index 3fd56b3..8b58ac3 100644 --- a/Tests/SequenceSetTest.cs +++ b/Tests/SequenceSetTest.cs @@ -1,6 +1,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Collections.Generic; +using S22.Imap; namespace S22.Imap.Test { /// @@ -68,6 +69,16 @@ public void RangesAndUIDs() { Assert.AreEqual("1,3:5,7", Util.BuildSequenceSet(list)); } + /// + /// Ensures a single UID is properly converted. + /// + [TestMethod] + [TestCategory("BuildSequenceSet")] + public void SingleUID() { + var list = new List() { 4 }; + Assert.AreEqual("4", Util.BuildSequenceSet(list)); + } + /// /// Passing null to Util.BuildSequenceSet should raise an ArgumentNullException. /// diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index c342e78..6d9c9e1 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -41,9 +41,6 @@ $(DefineConstants.Remove($(DefineConstants.LastIndexOf(";NET"))));$(TargetFrameworkVersion.Replace("v", "NET").Replace(".", "")) - - ..\bin\Debug\S22.Imap.dll - @@ -90,6 +87,12 @@ Resources.Designer.cs + + + {369c32a5-e099-4bd5-bbbf-51713947ca99} + S22.Imap + + diff --git a/Util.cs b/Util.cs index edf627a..0909eb9 100644 --- a/Util.cs +++ b/Util.cs @@ -64,8 +64,7 @@ internal static string[] ToChunks(this string str, int characters) { /// true if the value parameter occurs within this string, or if value is the empty /// string (""); otherwise, false. /// The value parameter is null. - internal static bool Contains(this string str, string value, - StringComparison comparer) { + internal static bool Contains(this string str, string value, StringComparison comparer) { return str.IndexOf(value, comparer) >= 0; } @@ -136,8 +135,7 @@ internal static void ThrowIfNullOrEmpty(this string s, string name) { /// Extension method for BinaryReader. /// Set to true to interpret the short value as big endian value. /// The 16-byte unsigned short value read from the underlying stream. - internal static ushort ReadUInt16(this BinaryReader reader, - bool bigEndian) { + internal static ushort ReadUInt16(this BinaryReader reader, bool bigEndian) { if (!bigEndian) return reader.ReadUInt16(); int ret = 0; @@ -219,8 +217,7 @@ internal static string DecodeWord(string word) { case "B": return encoding.GetString(Util.Base64Decode(text)); default: - throw new FormatException("Encoding not recognized " + - "in encoded word: " + word); + throw new FormatException("Encoding not recognized in encoded word: " + word); } } @@ -335,7 +332,7 @@ internal static string UTF7Encode(string s) { /// /// The UTF-7 encoded string to decode. /// A UTF-16 encoded "standard" C# string - /// Thrown if the input string is not a proper UTF-7 encoded + /// The input string is not a properly UTF-7 encoded /// string. /// IMAP uses a modified version of UTF-7 for encoding international mailbox names. For /// details, refer to RFC 3501 section 5.1.3 (Mailbox International Naming Convention). @@ -361,7 +358,7 @@ internal static string UTF7Decode(string s) { builder.Append(Encoding.BigEndianUnicode.GetString(buffer)); } catch (Exception e) { throw new FormatException( - "The input string is not in the correct Format", e); + "The input string is not in the correct Format.", e); } } else { if (c == '&' && reader.Peek() == '-')