IMAP IDLE模式(推送邮件)
在电子邮件技术中,IDLE是RFC 2177中描述的一项IMAP功能,它允许客户端向服务器表明它已准备好接受实时通知。
Internet消息访问协议IMAP4协议,它要求客户端轮询服务器来更改所选中的文件夹(如拉取新邮件、删除邮件),如果能让服务器推送通知客户端,告知客户端有新邮件的话会更方便客户端,尤其是在手机端的时候,大量的轮询查询服务器会耗费电量和流量,用户是不太允许这样做的,而且也不是很及时的收到邮件。
考虑到这种情况,其实IMAP4的扩展协议中是支持这个推送模式,即IMAP的IDLE模式。
首先我们用CAPABILITY 命令查询一下是否支持IDLE模式,因为并不是所有邮箱多支持的。
如qq邮箱:["CAPABILITY", "IMAP4", "IMAP4rev1", "IDLE", "XAPPLEPUSHSERVICE", "AUTH=LOGIN", "NAMESPACE", "CHILDREN", "ID", "UIDPLUS"]就支持这种模式,而163邮箱:["CAPABILITY", "IMAP4rev1", "XLIST", "SPECIAL-USE", "ID", "LITERAL+", "STARTTLS", "XAPPLEPUSHSERVICE", "UIDPLUS", "X-CM-EXT-1"]并不支持。
我们就用qq邮箱来测试一下,测试前请开通QQ邮箱的imap协议功能保持连接正常,关于IDLE命令的使用,需要先登录验证后,选中文件夹之后才可以使用,具体测试如下图:
查看命令可以知道,每次收到新邮件这会更改EXISTS的数量,这样就收到一个邮件通知,然后通过这个通知在去拉取邮件就可以。这是基本原理,具体到具体应用,由于我一直使用mailkit来获取邮件,而mailkit本身也是支持这种模式的。
mailkit具体代码如下:
1 namespace TestMailKit 2 { 3 public partial class Form2 : Form 4 { 5 public Form2() 6 { 7 InitializeComponent(); 8 } 9 10 private void button1_Click(object sender, EventArgs e) 11 { 12 TodoMail(); 13 } 14 15 public static void TodoMail() 16 { 17 try 18 { 19 using (var client = new ImapClient(new ProtocolLogger(Console.OpenStandardError()))) 20 { 21 client.Connect("imap.qq.com", 993, true); 22 if (client.AuthenticationMechanisms.Contains("XOAUTH2")) 23 client.AuthenticationMechanisms.Remove("XOAUTH2"); 24 client.Authenticate("110xxxxx31@qq.com", "******chfcf"); 25 26 client.Inbox.Open(FolderAccess.ReadOnly); 27 28 // Get the summary information of all of the messages (suitable for displaying in a message list). 29 var messages = client.Inbox.Fetch(0, -1, MessageSummaryItems.Full | MessageSummaryItems.UniqueId).ToList(); 30 31 // Keep track of messages being expunged so that when the CountChanged event fires, we can tell if it's 32 // because new messages have arrived vs messages being removed (or some combination of the two). 33 client.Inbox.MessageExpunged += (sender, e) => 34 { 35 var folder = (ImapFolder)sender; 36 37 if (e.Index < messages.Count) 38 { 39 var message = messages[e.Index]; 40 41 Console.WriteLine("{0}: expunged message {1}: Subject: {2}", folder, e.Index, message.Envelope.Subject); 42 43 // Note: If you are keeping a local cache of message information 44 // (e.g. MessageSummary data) for the folder, then you'll need 45 // to remove the message at e.Index. 46 messages.RemoveAt(e.Index); 47 } 48 else 49 { 50 Console.WriteLine("{0}: expunged message {1}: Unknown message.", folder, e.Index); 51 } 52 }; 53 54 // Keep track of changes to the number of messages in the folder (this is how we'll tell if new messages have arrived). 55 client.Inbox.CountChanged += (sender, e) => 56 { 57 // Note: the CountChanged event will fire when new messages arrive in the folder and/or when messages are expunged. 58 var folder = (ImapFolder)sender; 59 60 Console.WriteLine("The number of messages in {0} has changed.", folder); 61 62 // Note: because we are keeping track of the MessageExpunged event and updating our 63 // 'messages' list, we know that if we get a CountChanged event and folder.Count is 64 // larger than messages.Count, then it means that new messages have arrived. 65 if (folder.Count > messages.Count) 66 { 67 Console.WriteLine("{0} new messages have arrived.", folder.Count - messages.Count); 68 69 // Note: your first instict may be to fetch these new messages now, but you cannot do 70 // that in an event handler (the ImapFolder is not re-entrant). 71 // 72 // If this code had access to the 'done' CancellationTokenSource (see below), it could 73 // cancel that to cause the IDLE loop to end. 74 } 75 }; 76 77 // Keep track of flag changes. 78 client.Inbox.MessageFlagsChanged += (sender, e) => 79 { 80 var folder = (ImapFolder)sender; 81 82 Console.WriteLine("{0}: flags for message {1} have changed to: {2}.", folder, e.Index, e.Flags); 83 }; 84 85 Console.WriteLine("Hit any key to end the IDLE loop."); 86 using (var done = new CancellationTokenSource()) 87 { 88 // Note: when the 'done' CancellationTokenSource is cancelled, it ends to IDLE loop. 89 var thread = new Thread(IdleLoop); 90 91 thread.Start(new IdleState(client, done.Token)); 92 93 Console.ReadKey(); 94 done.Cancel(); 95 thread.Join(); 96 } 97 98 if (client.Inbox.Count > messages.Count) 99 { 100 Console.WriteLine("The new messages that arrived during IDLE are:"); 101 foreach (var message in client.Inbox.Fetch(messages.Count, -1, MessageSummaryItems.Full | MessageSummaryItems.UniqueId)) 102 Console.WriteLine("Subject: {0}", message.Envelope.Subject); 103 } 104 105 client.Disconnect(true); 106 } 107 } 108 catch (Exception ex) 109 { 110 Console.WriteLine(ex.Message); 111 } 112 } 113 114 static void IdleLoop(object state) 115 { 116 var idle = (IdleState)state; 117 118 lock (idle.Client.SyncRoot) 119 { 120 // Note: since the IMAP server will drop the connection after 30 minutes, we must loop sending IDLE commands that 121 // last ~29 minutes or until the user has requested that they do not want to IDLE anymore. 122 // 123 // For GMail, we use a 9 minute interval because they do not seem to keep the connection alive for more than ~10 minutes. 124 while (!idle.IsCancellationRequested) 125 { 126 // Note: Starting with .NET 4.5, you can make this simpler by using the CancellationTokenSource .ctor that 127 // takes a TimeSpan argument, thus eliminating the need to create a timer. 128 using (var timeout = new CancellationTokenSource()) 129 { 130 using (var timer = new System.Timers.Timer(9 * 60 * 1000)) 131 { 132 // End the IDLE command when the timer expires. 133 timer.Elapsed += (sender, e) => timeout.Cancel(); 134 timer.AutoReset = false; 135 timer.Enabled = true; 136 137 try 138 { 139 // We set the timeout source so that if the idle.DoneToken is cancelled, it can cancel the timeout 140 idle.SetTimeoutSource(timeout); 141 142 if (idle.Client.Capabilities.HasFlag(ImapCapabilities.Idle)) 143 { 144 // The Idle() method will not return until the timeout has elapsed or idle.CancellationToken is cancelled 145 idle.Client.Idle(timeout.Token, idle.CancellationToken); 146 } 147 else 148 { 149 // The IMAP server does not support IDLE, so send a NOOP command instead 150 idle.Client.NoOp(idle.CancellationToken); 151 152 // Wait for the timeout to elapse or the cancellation token to be cancelled 153 WaitHandle.WaitAny(new[] { timeout.Token.WaitHandle, idle.CancellationToken.WaitHandle }); 154 } 155 } 156 catch (OperationCanceledException) 157 { 158 // This means that idle.CancellationToken was cancelled, not the DoneToken nor the timeout. 159 break; 160 } 161 catch (ImapProtocolException) 162 { 163 // The IMAP server sent garbage in a response and the ImapClient was unable to deal with it. 164 // This should never happen in practice, but it's probably still a good idea to handle it. 165 // 166 // Note: an ImapProtocolException almost always results in the ImapClient getting disconnected. 167 break; 168 } 169 catch (ImapCommandException) 170 { 171 // The IMAP server responded with "NO" or "BAD" to either the IDLE command or the NOOP command. 172 // This should never happen... but again, we're catching it for the sake of completeness. 173 break; 174 } 175 finally 176 { 177 // We're about to Dispose() the timeout source, so set it to null. 178 idle.SetTimeoutSource(null); 179 } 180 } 181 } 182 } 183 } 184 } 185 186 } 187 188 class IdleState 189 { 190 readonly object mutex = new object(); 191 CancellationTokenSource timeout; 192 193 /// <summary> 194 /// Get the cancellation token. 195 /// </summary> 196 /// <remarks> 197 /// <para>The cancellation token is the brute-force approach to cancelling the IDLE and/or NOOP command.</para> 198 /// <para>Using the cancellation token will typically drop the connection to the server and so should 199 /// not be used unless the client is in the process of shutting down or otherwise needs to 200 /// immediately abort communication with the server.</para> 201 /// </remarks> 202 /// <value>The cancellation token.</value> 203 public CancellationToken CancellationToken { get; private set; } 204 205 /// <summary> 206 /// Get the done token. 207 /// </summary> 208 /// <remarks> 209 /// <para>The done token tells the <see cref="Program.IdleLoop"/> that the user has requested to end the loop.</para> 210 /// <para>When the done token is cancelled, the <see cref="Program.IdleLoop"/> will gracefully come to an end by 211 /// cancelling the timeout and then breaking out of the loop.</para> 212 /// </remarks> 213 /// <value>The done token.</value> 214 public CancellationToken DoneToken { get; private set; } 215 216 /// <summary> 217 /// Get the IMAP client. 218 /// </summary> 219 /// <value>The IMAP client.</value> 220 public ImapClient Client { get; private set; } 221 222 /// <summary> 223 /// Check whether or not either of the CancellationToken's have been cancelled. 224 /// </summary> 225 /// <value><c>true</c> if cancellation was requested; otherwise, <c>false</c>.</value> 226 public bool IsCancellationRequested 227 { 228 get 229 { 230 return CancellationToken.IsCancellationRequested || DoneToken.IsCancellationRequested; 231 } 232 } 233 234 /// <summary> 235 /// Initializes a new instance of the <see cref="IdleState"/> class. 236 /// </summary> 237 /// <param name="client">The IMAP client.</param> 238 /// <param name="doneToken">The user-controlled 'done' token.</param> 239 /// <param name="cancellationToken">The brute-force cancellation token.</param> 240 public IdleState(ImapClient client, CancellationToken doneToken, CancellationToken cancellationToken = default(CancellationToken)) 241 { 242 CancellationToken = cancellationToken; 243 DoneToken = doneToken; 244 Client = client; 245 246 // When the user hits a key, end the current timeout as well 247 doneToken.Register(CancelTimeout); 248 } 249 250 /// <summary> 251 /// Cancel the timeout token source, forcing ImapClient.Idle() to gracefully exit. 252 /// </summary> 253 void CancelTimeout() 254 { 255 lock (mutex) 256 { 257 if (timeout != null) 258 timeout.Cancel(); 259 } 260 } 261 262 /// <summary> 263 /// Set the timeout source. 264 /// </summary> 265 /// <param name="source">The timeout source.</param> 266 public void SetTimeoutSource(CancellationTokenSource source) 267 { 268 lock (mutex) 269 { 270 timeout = source; 271 272 if (timeout != null && IsCancellationRequested) 273 timeout.Cancel(); 274 } 275 } 276 } 277 278 }
通过上面的代码即可订阅邮件通知,方便的获取邮件。