(MVC) MVC (2019)

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 ( )
<00>  <01>  <02>  <03>  <04>  <05>  <06>  <07>  <08>  <09>  <10>  <11>  <12>  <13>  <14>  <15>  <16>  <17>  <18>  <19
Link to this page: http://www.vb-net.com/MailKit-IdleClient/Index.htm
<SITEMAP>  <MVC>  <ASP>  <NET>  <DATA>  <KIOSK>  <FLEX>  <SQL>  <NOTES>  <LINUX>  <MONO>  <FREEWARE>  <DOCS>  <ENG>  <MAIL ME>  <ABOUT ME>  < THANKS ME>