ASP.NET Core BackEnd with MailKit/IMAP (Async/Await, Monitor, ConcurrentQueue, CancellationTokenSource)
This is one of my typical job - this is BackEnd to Podio CRM. I do not write this code from beginning, I only doing full refactoring of existing system. This CRM was working before my refactoring, but with high payload it was crashed with some error.
I give backend code, transform it and return it to podio. This is some screen for debugging process.
And this is finish of my work.
Below I publish small (but most interesting) part of code. First part is controller code for testing.
127: [HttpPost]
128: public async Task<string> WaitForNewMessagesAsync([FromBody] TestMailHook hook)
129: {
130:
131: var emailDetails = new EmailAddressDetails
132: {
133: Address = hook.Address,
134: Password = hook.Password,
135: Provider = hook.Provider
136: };
137: string emailDetailsInbox = JsonConvert.SerializeObject(emailDetails);
138: string emailDetailsSent = JsonConvert.SerializeObject(emailDetails);
139: //Task taskA = new Task(() => Email.StartIdle("Inbox", emailDetailsInbox));
140: //taskA.Start();
141: var val = await Task.Run(() => Email.StartIdle("Inbox", emailDetailsInbox));
142: return val;
143: }
This is part of static class Email. Description of my changing you can see above in point 1.
140: public static string StartIdle(string folderName, string emailDetailsJSON)
141: {
142: EmailAddressDetails emailDetails = JsonConvert.DeserializeObject<EmailAddressDetails>(emailDetailsJSON);
143:
144: emailDetails.FolderName = folderName;
145: emailDetails.Host = IMAP_Server(emailDetails.Provider);
146:
147: if (CurrentEmails.Count(X => X.Address == emailDetails.Address && X.FolderName == emailDetails.FolderName) == 0)
148: {
149: Debug.WriteLine($"{emailDetails.FolderName}({emailDetails.Address}) - IdleClient({ Thread.CurrentThread.ManagedThreadId})");
150: CurrentEmails.Enqueue(emailDetails);
151: }
152: else
153: {
154: Debug.WriteLine($"{emailDetails.FolderName}({emailDetails.Address}) - MailBox now listening({ Thread.CurrentThread.ManagedThreadId})");
155: return "MailBox now listening";
156: }
157:
158: ImapServerNotSupportIdle = new CancellationTokenSource();
159: WrongAuthentication = new CancellationTokenSource();
160: var ClientMustFinish = CancellationTokenSource.CreateLinkedTokenSource(ImapServerNotSupportIdle.Token, WrongAuthentication.Token);
161:
162: using (var client = new IdleClient())
163: {
164: try
165: {
166: var idleTask = client.RunAsync(emailDetails);
167: System.Threading.Tasks.Task.Run(() =>
168: {
169: Console.ReadKey(true);
170: }).Wait(ClientMustFinish.Token);
171: }
172: catch (OperationCanceledException)
173: {
174: client.Exit();
175: EmailAddressDetails finished1;
176: CurrentEmails.TryDequeue(out finished1);
177: return "ImapServer not support IDLE mode or WrongAuthentication";
178: }
179: catch (Exception ex)
180: {
181: client.Exit();
182: EmailAddressDetails finished2;
183: CurrentEmails.TryDequeue(out finished2);
184: return ex.Message;
185: }
186: client.Exit();
187: EmailAddressDetails finished;
188: CurrentEmails.TryDequeue(out finished);
189: return "OK";
190: }
191: }
192: }
And this is class Idle Client after refactoring.
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Threading.Tasks;
5: using System.IO;
6: using System.Diagnostics;
7: using System.Threading;
8: using MailKit;
9: using MailKit.Security;
10: using MailKit.Net.Imap;
11: using MimeKit;
12: using PodioAPI.Models;
13: using PodioAPI.Utils.ItemFields;
14: using Newtonsoft.Json;
15: using Microsoft.Diagnostics.Tracing;
16:
17: namespace _MailKit
18: {
19: class IdleClient : IDisposable
20: {
21: List<IMessageSummary> messages;
22: CancellationTokenSource Cancel;
23: CancellationTokenSource dropConnection;
24: SharedThreadSafeValue<bool> messagesArrived;
25: ImapClient client;
26:
27: public IdleClient()
28: {
29: client = new ImapClient(new ProtocolLogger(Console.OpenStandardError()));
30: messages = new List<IMessageSummary>();
31: Cancel = new CancellationTokenSource();
32: messagesArrived = new SharedThreadSafeValue<bool>(false);
33: //new TplEventListener(System.Diagnostics.Tracing.EventLevel.Critical);
34:
35: }
36:
37: public async System.Threading.Tasks.Task RunAsync(EmailAddressDetails emailDetails)
38: {
39: Debug.WriteLine($"{emailDetails.FolderName}({emailDetails.Address}) - RunAsync({ Thread.CurrentThread.ManagedThreadId})");
40: // connect to the IMAP server and get our initial list of messages
41: try
42: {
43: await ReconnectAsync(emailDetails);
44: await FetchMessageSummariesAsync(emailDetails, true);
45: }
46: catch (OperationCanceledException)
47: {
48: await client.DisconnectAsync(true);
49: return;
50: }
51:
52: // keep track of changes to the number of messages in the folder (this is how we'll tell if new messages have arrived).
53: emailDetails.MailFolder.CountChanged += delegate (object sender, EventArgs e) { OnCountChanged(sender, e, emailDetails); };
54:
55: // keep track of messages being expunged so that when the CountChanged event fires, we can tell if it's
56: // because new messages have arrived vs messages being removed (or some combination of the two).
57: emailDetails.MailFolder.MessageExpunged += delegate (object sender, MessageEventArgs e) { OnMessageExpunged(sender, e, emailDetails); };
58:
59: // keep track of flag changes
60: emailDetails.MailFolder.MessageFlagsChanged += delegate (object sender, MessageFlagsChangedEventArgs e) { OnMessageFlagsChanged(sender, e, emailDetails); };
61:
62: //StackTrace stackTrace = new StackTrace();
63: //for (int i = 0; i < stackTrace.FrameCount; i++) {
64: // Debug.WriteLine(stackTrace.GetFrame(i).GetMethod().Name);
65: //}
66:
67: await IdleAsync(emailDetails); // <====
68:
69: emailDetails.MailFolder.MessageFlagsChanged -= delegate (object sender, MessageFlagsChangedEventArgs e) { OnMessageFlagsChanged(sender, e, emailDetails); };
70: emailDetails.MailFolder.MessageExpunged -= delegate (object sender, MessageEventArgs e) { OnMessageExpunged(sender, e, emailDetails); };
71: emailDetails.MailFolder.CountChanged -= delegate (object sender, EventArgs e) { OnCountChanged(sender, e, emailDetails); };
72:
73: await client.DisconnectAsync(true);
74: }
75:
76:
77: async System.Threading.Tasks.Task ReconnectAsync(EmailAddressDetails emailDetails)
78: {
79: Debug.WriteLine($"{emailDetails.FolderName}({emailDetails.Address}) - Reconnectasync ({Thread.CurrentThread.ManagedThreadId})");
80: if (!client.IsConnected)
81: try
82: {
83: await client.ConnectAsync(emailDetails.Host, Email.Port, Email.SslOptions, Cancel.Token);
84:
85: }
86: catch (Exception ex)
87: {
88: throw;
89: }
90:
91: Debug.WriteLine($"{ emailDetails.FolderName}({ emailDetails.Address}) - IMAPserver Capabilities ({client.Capabilities})");
92: if ((client.Capabilities & ImapCapabilities.Idle) == 0)
93: {
94: Email.ImapServerNotSupportIdle.Cancel();
95: return;
96: //throw new Exception("ImapServer not support IDLE");
97: }
98:
99: if (!client.IsAuthenticated)
100: {
101: try
102: {
103: // Authenticate
104: await client.AuthenticateAsync(emailDetails.Address, emailDetails.Password, Cancel.Token);
105:
106: // Open Folder
107: if (emailDetails.FolderName == "Inbox")
108: {
109: emailDetails.MailFolder = client.Inbox;
110: await emailDetails.MailFolder.OpenAsync(FolderAccess.ReadOnly, Cancel.Token);
111: }
112:
113: else if (emailDetails.FolderName == "Sent")
114: {
115: // Sent Folder
116: if ((client.Capabilities & (ImapCapabilities.SpecialUse | ImapCapabilities.XList)) != 0)
117: {
118: emailDetails.MailFolder = client.GetFolder(SpecialFolder.Sent);
119: await emailDetails.MailFolder.OpenAsync(FolderAccess.ReadOnly);
120: }
121: else
122: {
123: emailDetails.MailFolder = Email.GetSentFolder(client);
124: await emailDetails.MailFolder.OpenAsync(FolderAccess.ReadOnly);
125: }
126: }
127: else Exit();
128:
129: }
130: catch (MailKit.Security.AuthenticationException)
131: {
132: Email.WrongAuthentication.Cancel();
133: return;//set Less secure app access
134: }
135: catch (ImapProtocolException ex)
136: {
137: //IMAP4rev1 Server logging out
138: throw;
139: }
140: catch (Exception ex)
141: {
142: throw;
143: }
144:
145: }
146: }
147:
148: async System.Threading.Tasks.Task FetchMessageSummariesAsync(EmailAddressDetails emailDetails, bool print)
149: {
150: Debug.WriteLine($"{emailDetails.FolderName}({emailDetails.Address}) - FetchMessageSummariesAsync({ Thread.CurrentThread.ManagedThreadId})");
151:
152: //bool lockTaken = false;
153: //Monitor.TryEnter(emailDetails.MailFolder, 1000, ref lockTaken);
154:
155: try
156: {
157: // fetch summary information for messages that we don't already have
158: if (emailDetails.StartingFolderCount == 0)
159: emailDetails.StartingFolderCount = emailDetails.MailFolder.Count;
160:
161: int startIndex = emailDetails.StartingFolderCount + messages.Count;
162:
163: emailDetails.fetched = emailDetails.MailFolder.Fetch(startIndex, -1, MessageSummaryItems.Full | MessageSummaryItems.UniqueId, Cancel.Token);
164:
165: foreach (var message in emailDetails.fetched)
166: {
167: if (print)
168: Debug.WriteLine($"{emailDetails.FolderName}({emailDetails.Address}) - New Message Subject: {message.Envelope.Subject}");
169: messages.Add(message);
170: var parseSuccess = uint.TryParse(message.UniqueId.ToString(), out uint UIDofGetMessage);
171:
172: if (parseSuccess && UIDofGetMessage >= 0)
173: {
174: var wholeMessage = emailDetails.MailFolder.GetMessage(message.UniqueId);
175: var recordResult = await Email.Record_Messages(emailDetails.FolderName, wholeMessage, UIDofGetMessage, emailDetails.ItemId_Member);
176: }
177: }
178:
179: }
180: catch (ImapProtocolException)
181: {
182: // protocol exceptions often result in the client getting disconnected
183: await ReconnectAsync(emailDetails);
184: }
185: catch (IOException)
186: {
187: // I/O exceptions always result in the client getting disconnected
188: await ReconnectAsync(emailDetails);
189: }
190: catch (Exception ex)
191: {
192: throw;
193: }
194:
195: //if (lockTaken) Monitor.Exit(emailDetails);
196:
197: }
198:
199: async System.Threading.Tasks.Task WaitForNewMessagesAsync(EmailAddressDetails emailDetails)
200: {
201: Debug.WriteLine($"{emailDetails.FolderName}({emailDetails.Address}) - WaitForNewMessagesAsync({ Thread.CurrentThread.ManagedThreadId})");
202:
203: do
204: {
205: try
206: {
207: if (client.Capabilities.HasFlag(ImapCapabilities.Idle))
208: {
209: // Note: IMAP servers are only supposed to drop the connection after 30 minutes, so normally
210: // we'd IDLE for a max of, say, ~29 minutes... but GMail seems to drop idle connections after
211: // about 10 minutes, so we'll only idle for 9 minutes.
212: using (dropConnection = new CancellationTokenSource(new TimeSpan(0, 9, 0)))
213: {
214: using (var linked = CancellationTokenSource.CreateLinkedTokenSource(Cancel.Token, dropConnection.Token))
215: {
216: await client.IdleAsync(linked.Token);
217:
218: // throw OperationCanceledException if the Cancel token has been Canceled.
219: Cancel.Token.ThrowIfCancellationRequested();
220: }
221: }
222: }
223: else
224: {
225: // Note: we don't want to spam the IMAP server with NOOP commands, so lets wait a minute
226: // between each NOOP command.
227: using (var timer = new System.Timers.Timer(60 * 1000)) //1m
228: {
229: timer.Elapsed += (sender, e) => Cancel.Cancel();
230: timer.AutoReset = false;
231: timer.Enabled = true;
232: await System.Threading.Tasks.Task.Delay(new TimeSpan(0, 1, 0), Cancel.Token);
233: client.NoOp(Cancel.Token);
234: Debug.Print($"{emailDetails.FolderName}({emailDetails.Address}) - NoOp({ Thread.CurrentThread.ManagedThreadId})");
235: }
236: }
237: break;
238: }
239: catch (ImapProtocolException)
240: {
241: // protocol exceptions often result in the client getting disconnected
242: await ReconnectAsync(emailDetails);
243: }
244: catch (IOException)
245: {
246: // I/O exceptions always result in the client getting disconnected
247: await ReconnectAsync(emailDetails);
248: }
249: } while (true);
250: }
251:
252: async System.Threading.Tasks.Task IdleAsync(EmailAddressDetails emailDetails)
253: {
254: Debug.WriteLine($"{emailDetails.FolderName}({emailDetails.Address}) - IdleAsync({ Thread.CurrentThread.ManagedThreadId})");
255: do
256: {
257: try
258: {
259: await WaitForNewMessagesAsync(emailDetails);
260:
261: if (messagesArrived.Value)
262: {
263: await FetchMessageSummariesAsync(emailDetails, true);
264: messagesArrived.Value = false;
265: }
266: }
267: catch (SynchronizationLockException)
268: {
269: Debug.Print("SynchronizationLockException");
270: //no interesting, no concurent thread, messsage receive in one thread
271: }
272: catch (OperationCanceledException)
273: {
274: break;
275: }
276: catch (Exception ex)
277: {
278: throw;
279: }
280: } while (!Cancel.IsCancellationRequested);
281: }
282:
283:
284: // Note: the CountChanged event will fire when new messages arrive in the folder and/or when messages are expunged.
285: async void OnCountChanged(object sender, EventArgs e, EmailAddressDetails emailDetails)
286: {
287: Debug.WriteLine($"{emailDetails.FolderName}({emailDetails.Address}) - OnCountChanged({ Thread.CurrentThread.ManagedThreadId})");
288:
289: var folder = (ImapFolder)sender;
290:
291: bool lockTaken = false;
292: Monitor.Enter(folder, ref lockTaken);
293:
294: try
295: {
296:
297:
298: // Note: because we are keeping track of the MessageExpunged event and updating our
299: // 'messages' list, we know that if we get a CountChanged event and folder.Count is
300: // larger than messages.Count, then it means that new messages have arrived.
301: if (folder.Count > emailDetails.StartingFolderCount + messages.Count)
302: {
303: int arrived = folder.Count - (emailDetails.StartingFolderCount + messages.Count);
304:
305: if (arrived > 1)
306: Debug.WriteLine($"{emailDetails.FolderName}({emailDetails.Address}) - \t{arrived} new messages have arrived.");
307: else
308: Debug.WriteLine($"{emailDetails.FolderName}({emailDetails.Address}) - \t1 new message has arrived.");
309:
310: // Note: your first instict may be to fetch these new messages now, but you cannot do
311: // that in this event handler (the ImapFolder is not re-entrant).
312: //
313: // Instead, Cancel the `done` token and update our state so that we know new messages
314: // have arrived. We'll fetch the summaries for these new messages later...
315: messagesArrived.Value = true;
316:
317: if (dropConnection != null)
318: dropConnection?.Cancel();
319: else
320: {
321: await ReconnectAsync(emailDetails);
322: }
323:
324: }
325: }
326: catch (ObjectDisposedException)
327: {
328: //no matter
329: }
330: catch (Exception)
331: {
332: throw;
333: }
334:
335: if (lockTaken) Monitor.Exit(folder);
336: }
337:
338: void OnMessageExpunged(object sender, MessageEventArgs e, EmailAddressDetails emailDetails)
339: {
340: var folder = (ImapFolder)sender;
341: Debug.WriteLine($"{emailDetails.FolderName}({emailDetails.Address}) - {folder}: message #{e.Index} has been expunged.");
342: emailDetails.StartingFolderCount--;
343: }
344:
345: void OnMessageFlagsChanged(object sender, MessageFlagsChangedEventArgs e, EmailAddressDetails emailDetails)
346: {
347:
348: var folder = (ImapFolder)sender;
349: Debug.WriteLine($"{emailDetails.FolderName}({emailDetails.Address}) - {folder}: flags have changed for message #{e.Index} ({e.Flags}).");
350: }
351:
352: public void Exit()
353: {
354: Cancel.Cancel();
355: }
356:
357: public void Dispose()
358: {
359: client.Dispose();
360: Cancel.Dispose();
361: dropConnection?.Dispose();
362: }
363: }
364: }
Comments (
)
Link to this page:
//www.vb-net.com/MailKit-IdleClient/Index.htm
|