Notifications - Push, Pull, Stream Notification #5


 Exchange 2013을 기반으로 개발을 진행하면서 맞닥뜨리게 된 케이스에 대해서 공유 하고자 한다. 이전에 잠깐 EWS(Exchange Web Service)를 통해 간단한 기능을 사용해본 경험이 전부라서 하나의 기능을 구현하기 위해 여러가지 방안에 대해서 바위에 계란치기로 직접 부딪혀 볼 수 밖에 없었다. 이런 힘들고 시간 싸움을 줄이는데 도움을 드리는데 일조 하고자 이곳에 공유 하고자 합니다. 비록 덜 정제 되고 문서 미비할 수 있지만 참고 사항으로 알아 두셨으면 합니다. 



 이번에는 Listener에 대해서 살펴 보자. 이 클래스는 MBX에서 보내준 Push 정보인 XML을 HttpListener을 통해 받아 ListenEventArgs로 변환해 주고 연결된 이벤트(EventReceived)를 통해 핸들러로 넘겨 주는 작업을 한다. 이제 아래 코드에서 Listener의 전체 코드를 살펴 보자

/// <summary>
/// Push Notification을 보내 주면 HttpListener을 통해 받은 데이터를 파싱하고 연결된 이벤트를 발생한다.
/// </summary>
public class PushConsoleListener
{
    #region Fidles
    /// <summary>
    /// HttpListener을 통해서 MBX에서 보내준 정보를 받는 역할을 한다.
    /// </summary>
    private HttpListener _Listener = null;
    private string _ListenURi = "";
    private int _ListenPort = int.Parse(ConfigurationManager.AppSettings["PullNotificationListenPort"].ToString()); //36728
    private List<string> _Requests;
    /// <summary>
    /// MBX에서 정보를 보내주고 넘겨줄 리턴 값
    /// </summary>
    private string _NotificationResponse = "";
    //내부 로그 작업에 필요한 객체
    private ClassLogger _Logger = null;
    private string _subscriptionId = "";
    #endregion
 
    //이벤트 델리게이트 선언
    public delegate void ListenEventHandler(object senderListenEventArgs a);
    //이벤트 핸들로 선언
    //별도의 핸들러에서 MBX에서 보내준 정보를 가지고 작업을 할 수 있도록 해준다.
    //다른 클래스에서는 XML파싱이나 HttpListenr에 대해서 전혀 고려할 필요 없어 이 이벤트 처리에 대해서만 신경 쓰면 된다.
    public event ListenEventHandler EventReceived;
 
    //노드를 찾아가기 쉽도록 하기 위해 선엄 됨
    const string m = "http://schemas.microsoft.com/exchange/services/2006/messages";
    const string t = "http://schemas.microsoft.com/exchange/services/2006/types";
 
    public PushConsoleListener(ClassLogger Logger)
    {
        #region 하드코딩(이해하기 쉽도록 수정 된 코드)
        _NotificationResponse = "<?xml version=\"1.0\" encoding=\"utf-8\"?>";
        _NotificationResponse += "<soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:m=\"http://schemas.microsoft.com/exchange/services/2006/messages\" xmlns:t=\"http://schemas.microsoft.com/exchange/services/2006/types\" xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">";
        _NotificationResponse += "  <soap:Body>";
        _NotificationResponse += "    <m:SendNotificationResult>";
        _NotificationResponse += "      <m:SubscriptionStatus>OK</m:SubscriptionStatus>";
        _NotificationResponse += "    </m:SendNotificationResult>";
        _NotificationResponse += "  </soap:Body>";
        _NotificationResponse += "</soap:Envelope>";
        #endregion
 
        #region 리소스 파일에서 읽어 옴
        //리소스에 포함된 XML 파일 정보륵 가져온다.
        //이 작업은 MBX에서 HttpListener로 보내주고 정상적으로 Listen을 했는지 여부를 보내주기 위한 정보를 가져온다.
        //예제를 들기 위한 코드로서 에러가 발생 한다면 하드코딩된 데이터를 가지고 작업하면 됩니다.
        StreamReader oReader = new StreamReader(Assembly.GetExecutingAssembly().GetManifestResourceStream("NoneProcess.XML.EWSSendNotificationResult.xml"));
        _NotificationResponse = oReader.ReadToEnd();
        oReader.Close();
        #endregion
 
        _Requests = new List<string>();
        _Listener = new HttpListener();
 
        _Logger = Logger;
 
        StartListening();
    }
 
    ~PushConsoleListener()
    {
        Close();
    }
 
    /// <summary>
    /// 핸들러가 연결되어 있으면 이벤트를 발생 시켜서 정보를 넘겨 준다.
    /// </summary>
    /// <param name="e"></param>
    protected virtual void OnNotificationReceived(ListenEventArgs e)
    {
        ListenEventHandler handler = EventReceived;
 
        if (handler != null)
        {
            //이벤트고 연결되어 있으면 발생 시킴.
            handler(thise);
        }
    }
 
    private void StartListening()
    {
        //환결 설정 파일에서 HttpListener이 어떤 서버에서 구동되는지 가져 온다.
        //예로 테스트나 구동되는 Host의 IP를 넣어 주면 된다.
        //HttpListener은 관리자 권한으로 실행 해야 정상적인 동작을 할 수 있다.
        string sMachineName = ConfigurationManager.AppSettings["PullNotificationListenerUrlPrefix"].ToString();   //127.0.0.1 Environment.MachineName;
        //try
        //{
        //    System.DirectoryServices.ActiveDirectory.Domain oDomain = System.DirectoryServices.ActiveDirectory.Domain.GetComputerDomain();
        //    if (!String.IsNullOrEmpty(oDomain.Name))
        //        sMachineName = sMachineName + "." + oDomain.Name;
        //}
        //catch { }
 
 
        //_ListenURi = "http://" + sMachineName + ":" + _ListenPort + "/" + AppDomain.CurrentDomain.ToString() + "/";
        _ListenURi = "http://" + sMachineName + ":" + _ListenPort + "/" + "NoneProcessEWSTest" + "/";
        _Listener.Prefixes.Clear();
        //HttpLisener에 Prefix를 세팅해서 응답 받을 수 있도록 세팅 함.
        _Listener.Prefixes.Add(_ListenURi);
 
        //호스팅 정보를 콘솔에 알려 준다.
        Console.WriteLine(_ListenURi);
 
        try
        {
            //HttpListenr을 시작한다.
            //위에서 세팅된 ListenURi 주소로 호스팅을 시작한다.
            _Listener.Start();
            //세팅된 Prefix로 접속하면 GetContext에서 AsyncCallback가 실행 되도록 한다.
            _Listener.BeginGetContext(new AsyncCallback(ListenerCallback), _Listener);
        }
        catch (Exception ex)
        {
            throw;
        }
    }
 
    /// <summary>
    /// 실행 될 포트
    /// </summary>
    public int Port
    {
        get
        {
            return _ListenPort;
        }
        set
        {
            if (value == _ListenPortreturn;
            _Listener.Stop();
            _ListenPort = value;
            StartListening();
        }
    }
 
    /// <summary>
    /// 실행 될 URI 주소
    /// </summary>
    public string URi
    {
        get
        {
            return _ListenURi;
        }
    }
 
    /// <summary>
    /// Subscription Id
    /// </summary>
    public string SubscriptionId
    {
        get
        {
            return _subscriptionId;
        }
        set
        {
            _subscriptionId = value;
        }
    }
 
    /// <summary>
    /// MBX에서 보내준 정보를 받을 때 마다 Callback가 실행 된다.
    /// </summary>
    /// <param name="result"></param>
    public void ListenerCallback(IAsyncResult result)
    {
        //SubscribeToPushNotifications 등록 시 설정 된 frequency 설정 값대로 특정 주기로 서비스가 살아 있는지 확인 한다.
        //정상적으로 동작하지 않으면 MBX에서는 Push를 중단한다.
        //Console.WriteLine("Exchange서버에서 listener가 살아 있는지 점검 : {0}", DateTime.Now.ToString());
        try
        {
            HttpListener listener = (HttpListener)result.AsyncState;
 
            System.Threading.Tasks.Task.Factory.StartNew(() =>
            {
                //결과값을 받아 오기 위한 처리
                HttpListenerContext context = listener.EndGetContext(result);
                //HttpListenerContext에서 Request객체를 할당
                HttpListenerRequest request = context.Request;
 
                //Console.WriteLine("Local end point: {0}", request.LocalEndPoint.ToString());
                //Console.WriteLine("Remote end point: {0}", request.RemoteEndPoint.ToString());
                                        
                string sRequest = "";
 
                using (StreamReader reader = new StreamReader(request.InputStream))
                {
                    //MBX에서 보내준 XML 파일을 읽어 온다.
                    sRequest = reader.ReadToEnd();
                }
 
                //파싱 및 이벤트 전달 하도록 한다.
                DetermineResponse(sRequestcontext.Responserequest);
            });
 
            _Listener.BeginGetContext(new AsyncCallback(ListenerCallback), _Listener);
        }
        catch (Exception ex) { Console.WriteLine(ex.Message); }
    }
 
    /// <summary>
    /// 파싱 및 이벤트 전달 하도록 한다.
    /// </summary>
    /// <param name="Request">XML 전문 정보</param>
    /// <param name="Response">HttpListenerResponse</param>
    /// <param name="request">HttpListenerRequest</param>
    private void DetermineResponse(string RequestHttpListenerResponse ResponseHttpListenerRequest request)
    {
        //정상적인 XML 파일인지 체크
        if (Request.Contains("exchange"&& Request.Contains("SendNotificationResponseMessage"))
        {
            //MBX에 정상적으로 이벤트를 받았다고 리턴 해줌.
            WriteSubscriptionResponse(Response);
            //파싱하고 이벤트 발생시킴
            RaiseEvents(Requestrequest);
        }
 
        Response.OutputStream.Flush();
        //Response의 Stream을 닫는다.
        Response.Close();
    }
 
    /// <summary>
    /// 정상적으로 이벤트를 받았다고 리턴 해준다.
    /// </summary>
    /// <param name="Response">HttpListenerResponse</param>
    private void WriteSubscriptionResponse(HttpListenerResponse Response)
    {
        //정상적이라고 Response에 넘긴다.
        byte[] buffer = System.Text.Encoding.UTF8.GetBytes(_NotificationResponse);
        Response.ContentLength64 = buffer.Length;
        Response.ContentType = "text/xml";
        using (Stream output = Response.OutputStream)
        {
            output.Write(buffer0buffer.Length);
        }
 
        Log(_NotificationResponse"Notification Response");
    }
 
    /// <summary>
    /// XML 파싱해서 HttpListenEventArgs를 생성하고 이벤트를 
    /// </summary>
    /// <param name="Notifications"></param>
    /// <param name="request"></param>
    private void RaiseEvents(string NotificationsHttpListenerRequest request)
    {
        // Process the notifications
        XmlDocument oNotifications = new XmlDocument();
        try
        {
            //XMLDocument로 읽어 들인다. - XML 객체로 인식하도록 한다.
            oNotifications.LoadXml(Notifications);
        }
        catch (Exception ex)
        {
            return;
        }
 
        var responseClassValue = "Error";
        try
        {
            //특정 테그를 검색해서 원하는 노드의 Attribute의 값을 가져온다.
            responseClassValue = oNotifications.GetElementsByTagName("SendNotificationResponseMessage"m)[0].Attributes["ResponseClass"].Value;
        }
        catch { }
 
        //성고 여부 체크
        if (responseClassValue == "Success")
        {
            XmlNodeList nodeListNotifications = oNotifications.GetElementsByTagName("Notification"m);
            if (nodeListNotifications.Count == 0return;
 
            //SubscriptionId를 가져온다.
            XmlNode nodeSubscriptionId = oNotifications.GetElementsByTagName("SubscriptionId"t)[0];
            string subscriptionId = nodeSubscriptionId.InnerText;
 
            // SubscriptionId를 체크해서 정합성 여부를 체크 한다.
            // 이곳에서는 여러 사용자에 대해서 이벤트를 처리 하도록 되어 있기에 정합성 체크를 무시 한다.
            //if (!String.IsNullOrEmpty(_subscriptionId))
            //{
            //    if (subscriptionId != _subscriptionId)
            //        return;
            //}
 
            foreach (XmlNode node in nodeListNotifications[0].ChildNodes)
            {
                //이벤트 종류에 따라 XML 정보가 상이하여 다르게 처리 해야 함.
                switch (node.LocalName)
                {
                    case "StatusEvent":
                        break;
 
                    case "NewMailEvent":
                        //NewMail 관련 이벤트를 발생 시키며
                        //XML에서 관련 정보를 파싱해서 ListenEventArgs instance를 생성해서 넘겨 준다.
                        OnNotificationReceived(new ListenEventArgs(subscriptionId,
                        EventType.NewMail,
                        ExtractItemId("ItemId"node), ExtractFolderId("ParentFolderId"node)) { RemortEndpoint = request.RemoteEndPoint.ToString(), Watermark = GetWatermark(node), PreviousWatermark = GetPreviousWatermark(node) });
                        break;
 
                    case "CreatedEvent":
                        //CreatedEvent 관련 이벤트를 발생 시키며
                        //XML에서 관련 정보를 파싱해서 ListenEventArgs instance를 생성해서 넘겨 준다.
                        if (node.InnerXml.Contains("ItemId"))
                        {
                            OnNotificationReceived(new ListenEventArgs(subscriptionId,
                                EventType.Created,
                                ExtractItemId("ItemId"node), ExtractFolderId("ParentFolderId"node)) { RemortEndpoint = request.RemoteEndPoint.ToString(), Watermark = GetWatermark(node), PreviousWatermark = GetPreviousWatermark(node) });
                        }
                        else
                        {
                            OnNotificationReceived(new ListenEventArgs(subscriptionId,
                                EventType.Created,
                                ExtractFolderId("FolderId"node), ExtractFolderId("ParentFolderId"node)) { RemortEndpoint = request.RemoteEndPoint.ToString(), Watermark = GetWatermark(node), PreviousWatermark = GetPreviousWatermark(node) });
                        }
                        break;
 
                    case "MovedEvent":
                        //MovedEvent 관련 이벤트를 발생 시키며
                        //XML에서 관련 정보를 파싱해서 ListenEventArgs instance를 생성해서 넘겨 준다.
                        if (node.InnerXml.Contains("ItemId"))
                        {
                            OnNotificationReceived(new ListenEventArgs(subscriptionId,
                                EventType.Moved,
                                ExtractItemId("ItemId"node), ExtractFolderId("ParentFolderId"node),
                                ExtractItemId("OldItemId"node), ExtractFolderId("OldParentFolderId"node)) { RemortEndpoint = request.RemoteEndPoint.ToString(), Watermark = GetWatermark(node), PreviousWatermark = GetPreviousWatermark(node) });
                        }
                        else
                        {
                            OnNotificationReceived(new ListenEventArgs(subscriptionId,
                                EventType.Moved,
                                ExtractFolderId("FolderId"node), ExtractFolderId("ParentFolderId"node),
                                ExtractFolderId("OldFolderId"node), ExtractFolderId("OldParentFolderId"node)) { RemortEndpoint = request.RemoteEndPoint.ToString(), Watermark = GetWatermark(node), PreviousWatermark = GetPreviousWatermark(node) });
                        }
                        break;
 
                    case "CopiedEvent":
                        //CopiedEvent 관련 이벤트를 발생 시키며
                        //XML에서 관련 정보를 파싱해서 ListenEventArgs instance를 생성해서 넘겨 준다.
                        if (node.InnerXml.Contains("ItemId"))
                        {
                            OnNotificationReceived(new ListenEventArgs(subscriptionId,
                                EventType.Copied,
                                ExtractItemId("ItemId"node), ExtractFolderId("ParentFolderId"node),
                                ExtractItemId("OldItemId"node), ExtractFolderId("OldParentFolderId"node)) { RemortEndpoint = request.RemoteEndPoint.ToString(), Watermark = GetWatermark(node), PreviousWatermark = GetPreviousWatermark(node) });
                        }
                        else
                        {
                            OnNotificationReceived(new ListenEventArgs(subscriptionId,
                                EventType.Copied,
                                ExtractFolderId("FolderId"node), ExtractFolderId("ParentFolderId"node),
                                ExtractFolderId("OldFolderId"node), ExtractFolderId("OldParentFolderId"node)) { RemortEndpoint = request.RemoteEndPoint.ToString(), Watermark = GetWatermark(node), PreviousWatermark = GetPreviousWatermark(node) });
                        }
                        break;
 
                    case "DeletedEvent":
                        //DeletedEvent 관련 이벤트를 발생 시키며
                        //XML에서 관련 정보를 파싱해서 ListenEventArgs instance를 생성해서 넘겨 준다.
                        if (node.InnerXml.Contains("ItemId"))
                        {
                            OnNotificationReceived(new ListenEventArgs(subscriptionId,
                                EventType.Deleted,
                                ExtractItemId("ItemId"node), ExtractFolderId("ParentFolderId"node),
                                ExtractItemId("OldItemId"node), ExtractFolderId("OldParentFolderId"node)) { RemortEndpoint = request.RemoteEndPoint.ToString(), Watermark = GetWatermark(node), PreviousWatermark = GetPreviousWatermark(node) });
                        }
                        else
                        {
                            OnNotificationReceived(new ListenEventArgs(subscriptionId,
                                EventType.Deleted,
                                ExtractFolderId("FolderId"node), ExtractFolderId("ParentFolderId"node),
                                ExtractFolderId("OldFolderId"node), ExtractFolderId("OldParentFolderId"node)) { RemortEndpoint = request.RemoteEndPoint.ToString(), Watermark = GetWatermark(node), PreviousWatermark = GetPreviousWatermark(node) });
                        }
                        break;
 
                    case "ModifiedEvent":
                        //ModifiedEvent 관련 이벤트를 발생 시키며
                        //XML에서 관련 정보를 파싱해서 ListenEventArgs instance를 생성해서 넘겨 준다.
                        if (node.InnerXml.Contains("ItemId"))
                        {
                            OnNotificationReceived(new ListenEventArgs(subscriptionId,
                                EventType.Modified,
                                ExtractItemId("ItemId"node), ExtractFolderId("ParentFolderId"node),
                                ExtractItemId("OldItemId"node), ExtractFolderId("OldParentFolderId"node)) { RemortEndpoint = request.RemoteEndPoint.ToString(), Watermark = GetWatermark(node), PreviousWatermark = GetPreviousWatermark(node) });
                        }
                        else
                        {
                            OnNotificationReceived(new ListenEventArgs(subscriptionId,
                                EventType.Modified,
                                ExtractFolderId("FolderId"node), ExtractFolderId("ParentFolderId"node),
                                ExtractFolderId("OldFolderId"node), ExtractFolderId("OldParentFolderId"node)) { RemortEndpoint = request.RemoteEndPoint.ToString(), Watermark = GetWatermark(node), PreviousWatermark = GetPreviousWatermark(node) });
                        }
                        break;
                }
            }
        }
        else
        {
            //subscriptionId를 가져온다.
            XmlNode nodeSubscriptionId = oNotifications.GetElementsByTagName("MessageXml"m)[0];
            string subscriptionId = nodeSubscriptionId.InnerText;
 
            XmlNode nodeMessageText = oNotifications.GetElementsByTagName("MessageText"m)[0];
            string messageText = nodeMessageText.InnerText;
 
            OnNotificationReceived(new ListenEventArgs(subscriptionIdtrue) { Xml = messageText });
        }
    }
 
    private ItemId ExtractItemId(string TagXmlNode Node)
    {
        // Extract an ItemId from the node
        string itemId = "";
        string changeKey = "";
 
        foreach (XmlNode node in Node.ChildNodes)
        {
            if (node.LocalName == Tag)
            {
                // This is our ItemId
                itemId = node.Attributes["Id"].Value;
                changeKey = node.Attributes["ChangeKey"].Value;
                break;
            }
        }
        if (!String.IsNullOrEmpty(itemId))
        {
            ItemId id = new ItemId(itemId);
            //id.ChangeKey = changeKey;
            return id;
        }
        return null;
    }
 
    /// <summary>
    /// XML에서 FolderId 가져오기
    /// </summary>
    /// <param name="Tag"></param>
    /// <param name="Node"></param>
    /// <returns></returns>
    private FolderId ExtractFolderId(string TagXmlNode Node)
    {
        string folderId = "";
        string changeKey = "";
 
        foreach (XmlNode node in Node.ChildNodes)
        {
            if (node.LocalName == Tag)
            {
                //ItemId
                folderId = node.Attributes["Id"].Value;
                changeKey = node.Attributes["ChangeKey"].Value;
                break;
            }
        }
 
        if (!String.IsNullOrEmpty(folderId))
        {
            FolderId id = new FolderId(folderId);
            //id.ChangeKey = changeKey;
            return id;
        }
        return null;
    }
 
    /// <summary>
    /// XML에서 워터마크 가져오기
    /// </summary>
    /// <param name="Node">XML</param>
    /// <returns></returns>
    public string GetWatermark(XmlNode Node)
    {
        string returnValue = string.Empty;
 
        foreach (XmlNode node in Node.ParentNode.ChildNodes)
        {
            if (node.LocalName == "StatusEvent")
            {
                foreach (XmlNode n in node.ChildNodes)
                {
                    if (n.LocalName == "Watermark")
                    {
                        returnValue = n.Value;
                    }
                }
            }
        }
 
        return returnValue;
    }
 
    /// <summary>
    /// XML에서 이전 워터마크 가져오기
    /// </summary>
    /// <param name="Node">XML</param>
    /// <returns></returns>
    public string GetPreviousWatermark(XmlNode Node)
    {
        string returnValue = string.Empty;
 
        foreach (XmlNode node in Node.ParentNode.ChildNodes)
        {
            if (node.LocalName == "PreviousWatermark")
            {
                returnValue = node.InnerText;
            }
        }
 
        return returnValue;
    }
 
    private void Log(string Detailsstring Description = "")
    {
        try
        {
            if (_Logger == nullreturn;
            _Logger.Log(DetailsDescription);
        }
        catch { }
    }
 
    public void Close()
    {
        // Release the listener
        try
        {
            _Listener.Stop();
            _Listener.Close();
        }
        catch { }
    }
}

[코드1]Listener의 전체 소스


 위 소스에서 XML을 파싱하는 부분의 코드는 EWS의 기본 로직에 따라 해당하는 ListenEventArgs를 생성하고 있다. 예로 XML 파싱에서 MovedEvents가 발생되었다고 판독이 되면 ItemId가 있는지 체크하고 있으면 기존 아이템처럼 수행하면 되고 아니면 Folder 처럼 동작하도록 되었다. Exchagne 에서는 모든 아이템이 동등한 객체처럼 관리가 되고 있기 때문이다. Folder가 옮겨져도 MovedEvents가 발생하고 메일이 옮겨져도 MovedEvents가 발생하기 때문이다. 다른 아이템도 마찬가지로 이벤트가 동작하게 되어 있다.


 이번 "코드1"의 소스 길이가 다른 사항에 비해 상단히 길게 작성이 되어 있다. 쉽게 이해할 수 없을 수도 있으나 디버깅이나 실행 흐름을 따라가다가 보면 어렵지 않게 이해할 수 있으리라 기대한다. 다만 조급히 생각하지 않고 시간 투자만 한다면 어렵지 않을것이다. 다만 이곳에서 직접적으로 보여줄 수 없는 부분이 서버에 대한 정보라고 할수 있을 것이다. ConfigurationManager을 통해서 설정 파일에서 읽어 오는 부분은 옆에 주석으로 해당 값에 대한 예시를 같이 적어 두었으니 구동 환경에 맞게 수정하면 될 것이다. 그리고 여기에 리소스로 포함되는 XML 파일을 보여주는 것으로 이번 Push Notification을 마치도록 하겠다.


<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <m:SendNotificationResult>
      <m:SubscriptionStatus>OK</m:SubscriptionStatus>
    </m:SendNotificationResult>
  </soap:Body>
</soap:Envelope>

[코드2] 리소스에 포함되는 XML 파일 ( MBX에 정상적으로 수신되었다고 리턴 값을 보내는 정보)








+ Recent posts