Silverlight и работа с IMAP по шифрованному SSL соединению

Что делать, если нужно подружить Silverlight In Browser Application и сокеты через SSL? Как быть, если еще и необходима поддержка протокола IMAP? А если требуется, чтобы работа с протоколом IMAP осуществлялась со стороны клиентского ПК?

В нашей статье  мы поделимся опытом и дадим ответы на эти вопросы.

Библиотека для работы с IMAP через SSL

Дело в том, что  библиотека классов Silverlight (3,4,5) весьма урезана по сравнению с библиотекой классов для WPF, множество реализаций классов отсутствует. Так, например, нет реализации классов для пространства имен  System.Net.Mail. Но даже если бы таковая и была, то там все равно нет поддержки протокола IMAP. Поэтому мы выбираем стороннюю библиотеку для работы с IMAP и поддержкой SSL, так как их представлено множество:

Мы выбрали AE.Net.Mail — компактную библиотеку, которая содержит то, что необходимо для работы. После скачивания мы пытаемся добавить сборки в проект, и умная IDE напоминает нам о том, что для проектов Silverlight подходят Silverlight сборки. Итак, есть два варианта:

Вариант 1

Разработать WCF или asmx (http only) сервис, добавить в него сборки, описать контракты…и вперед! Но тогда мы нарушим ограничение – работа с протоколом IMAP возможна только с клиентского ПК. Конечно, в Silverlight есть поддержка сокетов (System.Net.Sockets.Socket), но вот поддержки SSL нет, да и политики безопасности очень ограничивают работу с ними. Более детально о политиках безопасности вы можете узнать здесь. Если немного погуглить, то на просторах интернета можно найти ссылку на продукт SecureBlackbox, который поддерживает SSL в Silverlight (4,5), но вот поддержки IMAP протокола, к сожалению, здесь нет. Конечно, можно реализовать поддержку IMAP (общение с сервером определенным набором команд) самостоятельно, но это требует дополнительных трудозатрат. Да и продукт SecureBlackbox – не бесплатный, хотя есть пробная версия.

Вариант 2

Разработать Windows/Linux/Mac сервис, где будет хоститься WCF REST сервис, с которым будет общаться наше Silverlight приложение. Данная служба может быть инсталлирована при загрузке Silverlight приложения или при клике на отдельную ссылку на сайте, где оно будет размещено. Вот на нем мы и остановимся подробнее.

Создание Windows OS Service

На первом этапе нам была необходима поддержка Windows OS, поэтому далее мы опишем процесс создания сервиса для этой ОС. Добавляем новый проект в наше Silverlight решение или создаем новый из шаблона Windows – Windows Service. Visual Studio создает нам базовый шаблон для ОС сервиса.

static class Program
   {
       /// <summary>;
       /// The main entry point for the application.
       /// </summary>;
       static void Main()
       {
           ServiceBase[] ServicesToRun;
           ServicesToRun = new ServiceBase[]
           {
               new Service1()
           };
           ServiceBase.Run(ServicesToRun);
       }
   }

Далее создадим интерфейс для WCF REST сервиса.

namespace IMAPWinService
{
   [ServiceContract]
   public interface IIMAPService
   {
        [WebGet(UriTemplate ="Service/GetMessageList?host={host}&amp;username={username}&amp;password={password}&amp;startIndex={startIndex}&amp;endIndex={endIndex}", ResponseFormat = WebMessageFormat.Json)]
       List&lt;MessageModel&gt; Get_Messages(string host, string username, string password, string startIndex, string endIndex);
   }//end
}

Это простой интерфейс, в котором определен контракт для получения списка писем из почтового ящик через протокол IMAP. Атрибут WebGet говорит о том, что  операция службы логически является операцией извлечения, и что она может быть вызвана моделью программирования WCF REST. UriTemplate, как видно, использует шаблон querystring. Также нам понадобится DataContract:

   [DataContract(Namespace = "<a href="http://tempuri.org/">http://tempuri.org</a>")]
   public class MessageModel
   {
       [DataMember]
       public string UID { get; set; }
       [DataMember]
       public string Subject { get; set; }
       [DataMember]
       public string AttachmentName { get; set; }
       [DataMember]
       public DateTime CreateDate { get; set; }
   }

Теперь определяем сам класс. Он наследуется от интерфейса IMAPService:

[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
     public class IMAPService:IIMAPService
   {
public List&lt;MessageModel&gt; Get_Messages(string host, string username, string password, string startIndex, string endIndex)
          {
           var res = new List&lt;MessageModel&gt;();
           using (ImapClient = new AE.Net.Mail.ImapClient(host, username, password,AE.Net.Mail.ImapClient.AuthMethods.Login, 993, true))
           {
               ImapClient.SelectMailbox(BACKUPTHAT_MALBOX);
               var _startIndex = Convert.ToInt32(startIndex);
              var _endIndex = Convert.ToInt32(endIndex);
               var messages = ImapClient.GetMessages(_startIndex, _endIndex, true);
               var m_result = (from msg in messages
                               select new MessageModel
                               {
                                   UID = msg.Uid,
                                   Subject = msg.Subject,
                                   AttachmentName = msg.Subject,
                                   CreateDate = msg.Date
                               }).ToList();
               res = m_result.ToList();
              }
           return res;
       }
<p>public string SendFileToEmail(string host, string username, string password, string fileName)
       {
           var res = string.Empty;
           return res;
     }
}

Метод SendFileToEmail будет рассмотрен в следующей статье, так как там есть свои особенности. Далее добавим в проект файл конфигурации сервиса:

<?xml version="1.0"?>
<configuration>
   <startup>
       <supportedRuntime version=”v4.0″ sku=”.NETFramework,Version=v4.0″/>
   </startup>
   <system.serviceModel>
       <serviceHostingEnvironment aspNetCompatibilityEnabled=”true”/>
       <behaviors>
           <endpointBehaviors>
             <behavior name=”BehaviorRest”>
               <webHttp helpEnabled=”true”/>
             </behavior>
           </endpointBehaviors>
         <serviceBehaviors>
           <behavior name=”MyServiceTypeBehaviors”>
             <serviceMetadata httpGetEnabled=”true”/>
           </behavior>
         </serviceBehaviors>
       </behaviors>
     <bindings>
       <webHttpBinding>
         <binding name=”webHttpBindingWithJsonP” crossDomainScriptAccessEnabled=”true” closeTimeout=”01:01:00″ openTimeout=”01:01:00″ receiveTimeout=”01:10:00″ sendTimeout=”01:01:00″>
           <readerQuotas maxArrayLength=”400000000″/>
         </binding>
       </webHttpBinding>
     </bindings>
       <services>
           <service behaviorConfiguration=”MyServiceTypeBehaviors” name=”IMAPWinService.IMAPService”>
               <endpoint behaviorConfiguration=”BehaviorRest” binding=”webHttpBinding” bindingConfiguration=”webHttpBindingWithJsonP” contract=”IMAPWinService.IIMAPService”/>
               <host>
                   <baseAddresses>
                       <add baseAddress=”http://localhost:8000/”/>
                   </baseAddresses>
               </host>
           </service>
       </services>
   </system.serviceModel>
</configuration>

Обратите внимание на то, что мы используем binding=”webHttpBinding”, так как WCF сервис у нас является REST сервисом. Также необходимо использование атрибута crossDomainScriptAccessEnabled=”true” для включения поддержки кросс доменных запросов, ведь со стороны браузеров сайт с Silverlight приложением и ОС сервис работают в разных доменах. В качестве порта мы выбрали порт с номером 8000. Можете выбрать любой другой свободный порт или предлагать выбирать его при установке сервиса, но в этом случае код будет несколько отличаться.

После этого отредактируем класс сервиса, унаследованный от ServiceBase

namespace IMAPWinService
{
   public partial class WinIMAPService : ServiceBase
   {
       public ServiceHost serviceHost = null;
       public WinIMAPService()
       {
           InitializeComponent();
       }
       protected override void OnStart(string[] args)
       {
           if (serviceHost != null)
           {
               serviceHost.Close();
           }
           // Create a ServiceHost for the CalculatorService type and
           // provide the base address.
           serviceHost = new ServiceHost(typeof(IMAPService));
           // Open the ServiceHostBase to create listeners and start
           // listening for messages.
           serviceHost.Open();
       }
       protected override void OnStop()
       {
           if (serviceHost != null)
           {
               serviceHost.Close();
               serviceHost = null;
           }
       }
   }//end class
}

Данный класс реализует стандартное поведение службы Windows, переопределяя и реализуя методы  OnStart(string[] args) и OnStop(). В методе OnStart(string[] args) мы и реализуем хостинг WCF REST сервиса строкой кода:

serviceHost = new ServiceHost(typeof(IMAPService));

Теперь добавим Installer для нашего сервиса. Для этого нужно в дизайнере сервиса нажать на правую кнопку мыши и выбрать Add Installer в сервисе проектировщик. Visual Studio сгенерирует класс ProjectInstaller, унаследованный от System.Configuration.Install.Installer, который необходимо привести к следующему виду:

namespace IMAPWinService
{
   [RunInstaller(true)]
   public class ProjectInstaller : Installer
   {
       private ServiceProcessInstaller process;
       private ServiceProcessInstaller serviceProcessInstaller1;
       private ServiceInstaller serviceInstaller1;
       private ServiceInstaller service;
       public ProjectInstaller()
       {
           process = new ServiceProcessInstaller();
           process.Account = ServiceAccount.LocalSystem;
           service = new ServiceInstaller();
           service.ServiceName = "IMAPService";
           service.StartType=ServiceStartMode.Automatic;
           Installers.Add(process);
           Installers.Add(service);
       }
       private void InitializeComponent()
       {
           this.serviceProcessInstaller1 = new System.ServiceProcess.ServiceProcessInstaller();
           this.serviceInstaller1 = new System.ServiceProcess.ServiceInstaller();
           //
           // serviceProcessInstaller1
           //
           this.serviceProcessInstaller1.Password = null;
           this.serviceProcessInstaller1.Username = null;
           //
           // ProjectInstaller
           //
           this.Installers.AddRange(new System.Configuration.Install.Installer[] {
           this.serviceProcessInstaller1,
           this.serviceInstaller1});
       }
   }//end class
}

В данном классе мы задаем имя сервиса, учетную запись, под которой он будет работать, тип запуска и учетные данные (Username и Password). Теперь немного отредактируем статический класс — точку входа приложения сервиса.

namespace IMAPWinService
{
   static class Program
   {
       public static  string SERVICE_NAME = "IMAPService";
       /// &lt;summary&gt;
       /// The main entry point for the application.
       /// &lt;/summary&gt;
       static void Main(string[] args)
       {
           args=new string[2];
           args[0] = "-install";
           var service = GetCurrentServiceController(SERVICE_NAME);
           if (service != null) args[0] = "-uninstall";
           if (System.Environment.UserInteractive)
           {
               if (args.Length &gt; 0)
               {
                   switch (args[0])
                   {
                       case "-install":
                           {
                               ManagedInstallerClass.InstallHelper(new string[] { Assembly.GetExecutingAssembly().Location });
                               service = GetCurrentServiceController(SERVICE_NAME);
                               if (service.Status != ServiceControllerStatus.Running)
                               {
                                   service.Start();
                                   service.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromSeconds(10));
                               }
                               break;
                           }
                       case "-uninstall":
                           {
                               if (service.Status == ServiceControllerStatus.Running)
                               {
                                   service.Stop();
                               }
                               //
                               ManagedInstallerClass.InstallHelper(new string[] { "/u", Assembly.GetExecutingAssembly().Location });
                               //
                               Thread.Sleep(3000);
                               ManagedInstallerClass.InstallHelper(new string[] { Assembly.GetExecutingAssembly().Location });
                               //
                               Thread.Sleep(3000);
                               //
                               service.Start();
                               break;
                           }
                   }
               }
           }
           else
           {
               ServiceBase[] ServicesToRun;
               ServicesToRun = new ServiceBase[]
                                   {
                                       new WinIMAPService()
                                   };
              ServiceBase.Run(ServicesToRun);
           }
       }
       private static ServiceController GetCurrentServiceController(string serviceName)
       {
           ServiceController service = ServiceController.GetServices().FirstOrDefault(s =&gt; s.ServiceName == serviceName);
           return service;
       }
   }//end class
}

В данном классе применен некий workaround для того, чтобы сервис самостоятельно устанавливался, переустанавливался и запускался.

Теперь мы можем опубликовать сервис в указанную директорию. Для этого необходимо в Solution Explorer нажать правой кнопкой мыши на проект ОС сервиса и выбрать Publish. Затем указать директорию и нажать Finish либо пройти до конца по шагам мастера публикации.

Зайдя в директорию только что опубликованной службы, запускаем файл setup.exe от имени Администратора системы. После установки перейдем в оснастку служб Windows, чтобы убедиться, что служба установлена и запущена. Также в адресной строке браузера можем перейти на адрес http://localhost:8000/help и убедиться, что WCF REST сервис работает корректно. Имея данную службу, мы можем делать кросс-доменные Ajax запросы к нашему локальному WCF REST сервису, опубликованному внутри службы Windows из Silverlight приложения, например, с помощью jQquery framework, и получать ответы в формате JSON.

В следующей статье мы опишем данный процесс, а также отправку файлов посредством протокола IMAP.

Итоги

Имея данную службу, мы можем делать  кросс доменные Ajax запросы к нашему локальному WCF REST сервису, опубликованному внутри службы Windows. Эти запросы отправляются через Silverlight приложение и могут быть созданы с помощью JQuery фреймворка. Ответы получаем в формате JSON.