동적 사용자의 Push Notification 등록


목차
  1. 파워쉘로 사용자 정보 알아내기
  2. Push Notification 등록 요청


 이전 포스트에서 파워쉘로 Active 사용자를 알아 내는 방법을 살펴 보았다. ( 2013/05/03 - [기타] - [Exchange] 동적 사용자의 Push Notification 등록 #1 ) 이제 이번 포스트에서는 Push Notification을 신청하도록 하자.


아래 '코드1'을 통해 Active mode의 Database에서 사용자를 가져와 Push Notification을 요청하는 코드이다.

/// <summary> /// Active Database를 이용해서 사용자 알아 내기 /// </summary> private static void RequestDatabase() {     // 요청자 사용자 리스트     var requestList = new List<string>();     // 요청된 사용자의 SubscriptionId 리스트     var subscriptionIdList = new ConcurrentDictionary<stringstring>();     // Task 리스트     var taskList = new List<System.Threading.Tasks.Task>();     // 파워쉘 명령으로 알아 내기     using (var powershellInvoker = new ExchangePowerShellWrapper(new AppConfigExchangePowerShellConfig()))     {         // 파워쉘 실행         foreach (var databaseItem in powershellInvoker.PipelineScriptInvoke("Get-MailboxDatabaseCopyStatus -active | select-object DatabaseName"))         {             // DatabaseName을 추출한다.             var databaseName = databaseItem.Members["DatabaseName"].Value.ToString();             // Get-Mailbox 파워쉘을 실행하여 사용자의 WindowsEmailAddress를 알아 낸다.             foreach (var user in powershellInvoker.PipelineScriptInvoke("Get-Mailbox -Database \"" + databaseName + "\" | select-object WindowsEmailAddress"))             {                 // 사용자의 이메일 주소                 var windowsEmailAddress = user.Members["WindowsEmailAddress"].Value.ToString();                 var databaseNameTemp = databaseName;                 var temp = System.Threading.Tasks.Task.Factory.StartNew(() =>                 {                     //GetRequest(databaseNameTemp, windowsEmailAddress);                     // List에 관리가 되도록 추가 한다.                     subscriptionIdList.TryAdd(GetRequestNotification(windowsEmailAddress), windowsEmailAddress);                 });                 // waiting 하기 위해 리스트에 넣어 둔다.                 taskList.Add(temp);             }         }     }     // 작업이 완료 될 때까지 대기 하도록 한다.     System.Threading.Tasks.Task.WaitAll(taskList.ToArray()); } /// <summary> /// 웹 서비스로 Subscription 신청 /// </summary> /// <param name="smtpAddress">메일 주소</param> private static string GetRequestNotification(string smtpAddress) {     try     {         string subscriptionId = null;         PushSubscription pushSubscription = null;         var ewsService = new ExchangeService(Microsoft.Exchange.WebServices.Data.ExchangeVersion.Exchange2013)         {             // 인증 정보 세팅             Credentials = new NetworkCredential("domain\\id""password")         };         // 이벤트 종류         var events = new List<EventType>() { EventType.CreatedEventType.Deleted };         // 폴더 범위         var folders = new List<FolderId>() { new FolderId(WellKnownFolderName.Inbox) };         pushSubscription = ewsService.SubscribeToPushNotifications(folders.ToArray(),                                                     new Uri("https://owa.mail.com/ews/exchange.asmx"),                                                     1,                                                     null,                                                     events.ToArray());         // Push Notification 요청하면 관리가 되도록 uniqueId를 반환한다.         subscriptionId = pushSubscription.Id;         return subscriptionId;     }     catch (Exception ex)     {         Console.WriteLine("{0}, {1}, {2}"smtpAddressex.Messagecount++);         throw;     } }

[코드1] Active mode database 사용자 추가


  RequestDatabase() 함수에서는 Database에서 사용자 정보를 추출하고, GetRequestNotification() 함수에서는 Exchange에 Push Notification 요청한다.






동적 사용자의 Push Notification 등록


목차
  1. 파워쉘로 사용자 정보 알아내기
  2. Push Notification 등록 요청


 여러 익스체인지 서버가 동시에 서비스가 되고 있고 Active Db, Passive Db가 서로간의 서버간에 복제가 되도록 구성이 되었을 때 Push Notification을 어떻게 등록할 것인지에 대한 한정된 주제에 대해서만 언급하도록 하겠다. 이 제한된 구성은 수 많은 MBX서버가 고 가용성을 위해 다른 서버에게 복제가 되도록 구성이 된 구조에서 파워쉘을 이용해 Active 된 Database 사용자만을 가져와 Push notification을 요청 하도록 한다.


 이제 진행해 보도록 하자. MBX 서버에는 내부적으로 Active mode Database가 있고 Passive mode Database가 존재하고 있다. 그중에서 메일을 직접적으로 받고 있는 사용자는 Active mode Database에 있는 사용자만이 익스체인지 MBX에서 메일을 받고 처리하며 받은 메일에 대해서 다른 서버에 복제되도록 구성된 Database에 복사가 된다. 그렇다면 현재 서버에서 Active mode의 Database를 가져오는 방법을 우선 알아야 할 것이다. 아래 '코드1'과 같은 방법으로 활성화된 Database를 가져올 수가 있다.

Get-MailboxDatabaseCopyStatus -active | select-object DatabaseName

[코드1] Active mode Database


 이 코드를 수행하면 현재 서버에서 활성화된 Database를 알수가 있다. ( DatabaseName만 필요하기에 Select-Object를 통해 필터링을 하였다 )



[그림1] '코드1'의 수행 결과


 이렇게 알아낸 DatabaseName중에 'HADB01' 사용자의 정보를 알아 내는 구문은 '코드2'와 같이 실행하면 나온다.


Get-Mailbox -Database "HADB01"

[코드2] 사용자 알아 내기




[그림2] 사용자 알아내기 수행 결과 화면


 위와 같은 방법으로 현 서버에서 작업이 되는 사용자의 정보를 알아 낼 수 있게 되었다. 이 정보를 가지고 Push Notification 하는 방법은 다음 포스트에서 이어 가도록 하겠다.





ManagementEventWatcher Process monitoring


참조 URL
  1. ManagementEventWatcher class
  2. ManagementObjectSearcher class
  3. ManagementScope class
  4. SelectQuery class
  5. ObjectQueryObjectQuery<T>


 이 포스트에 있는 내용이 언제나 확실한 정답은 아닙니다. 진실이라고 생각해 왔던 전제가 시간의 지남에 따라 들어나지 않았던 다른 이면 때문에 좋은 방향으로 이끌어 낼 수 있는 역할로 변환 되는게 역사적으로도 많은 증명 있었습니다. 그렇지만 저는 현재 상황에서 최선의 답을 찾고자 노력하였으며 이 글을 읽는 다른 분들에게 다음 길을 갈 수 있도록 도와주는 디딤돌이 되고자 노력하고자 포스팅을 통해 공유하고자 하는 것입니다. 그리고 프로그래머라는 타이틀을 달고 살아야 한다면 "왜"라는 의문을 항상 가지고 다니면서 자신의 위치에 안주하지 않고 항상 노력하는 모습으로 살아 가고자 합니다. 언제든 지적이나 오류가 있으면 피드백 부탁 드리겠습니다.

ing™       



2013/05/03 - [기타] - [WMI] ManagementObjectSearcher Process monitoring - 프로세스 모니터링


 이 포스트에서는 이미 실행되어 있는 프로세스에 대한 정보에 대해서 처리하는 내용이었다면 이번 포스트는 이벤트 방식으로 새로 실행되거나 종료될 때 처리할 수 있는 방안이다. 아래 '코드1'은 이벤트를 선언하는 부분이다.

// 모니터링 하고자 하는 프로세스 명 string monitoringProcessName = "notepad.exe"; // 이벤트 객체 선언 ManagementEventWatcher startWatcherProcess; //// Nodepad process가 시작할 때 이벤트 처리 startWatcherProcess = new ManagementEventWatcher(new WqlEventQuery("SELECT * FROM Win32_ProcessStartTrace WHERE ProcessName = '" + monitoringProcessName + "'"));   // WHERE ProcessName = 'microsoft.exchange.store.worker.exe' startWatcherProcess.EventArrived += startWatcher_EventArrived; startWatcherProcess.Start();

[코드1] 이벤트 선언 및 Query 구문 적용


 위 '코드1'은 노트패드의 프로세스가 시작이 되면 이벤트를 발생하도록 구성 되었다.


static void startWatcher_EventArrived(object senderEventArrivedEventArgs e) {     Console.WriteLine("Process started: {0}, {1}"e.NewEvent.Properties["ProcessName"].Valuee.NewEvent.Properties["ProcessID"].Value); }

[코드2] 시작 이벤트 처리 핸들러


 위와 같이 이벤트가 발생하면 핸들러에서 수행을 하여 처리할 수 있을 것이다. '코드1'의 Query 구문과 같이 시작 이벤트를 받을 수 있다면 종료 이벤트에 대해서도 받을 수 있도록 수정할 수 있다. "Win32_ProcessStartTrace"와 같이 Start 대신에 "Win32_ProcessStopTrace"와 같이 수정해 주면 종료 이벤트에 대해서도 처리할 수 있다.


소스 코드 자체에 주석과 직관적인 코딩으로 충분히 파악이 가능할 것으로 예상하므로 별도의 설명을 생략하도록 하겠습니다. 포스트의 내용이 장황한 설명 보다는 주석과 소스코드 자체 만으로도 이해할 수 있도록 하기 위해 노력하였습니다. 실 개발에서도 적용할 수 있도록 간단하면서도 현실적인 예제 프로그램을 통해 각 소스를 만들고 이해 시키고자 하였으며 실무에 필요한 개발요구 사항들을 해결 하는데 도움이 되고자 노력하였습니다. 그리고 소스와 같이 있는 주석을 이용해 nDoc이나 별도의 자동 Document 제작 유틸로 API 문서를 만드는 데에도 도움이 되었으면 한다. 
※ DOC에 대한 프로그램 정보 Util link

ing™       


Impersonation by Powershell


참조 URL
  1. Configuring Exchange Impersonation
  2. Configure Exchange Server 2010 Impersonation

 

 Exchange를 이용한 관련 프로그램을 개발하기 위해 EWS를 이용하게 될때 Impersonation 기능을 이용해 일반 사용자 계정으로 가장을 하여 사서함에 접근하도록 개발해야 한다. 가장을 하기 위해서는 해당 계정이 가장(Impersonation)을 할 수 있는 권한을 할당해 줘야 에러가 발생하지 않고 가장을 할 수 있다. 이 권한이 할당되어 있지 않으면 'ImpersonateionId = new ...' 할 때 에러가 발생한다. 이럴 때 아래와 같이 익스체인지 파워쉘에서 아래와 같이 입력해  가장 권한을 할당해 주면 에러 없이 개인 사서함에 접근해 정보에 접근할 수 있다.


New-ManagementRoleAssignment –Name:impersonationAssignmentName –Role:ApplicationImpersonation –User:serviceAccount 

[표1] 가장 권한 할당 예제


 위와 같이 설정을 하면 아래와 같은 코드에서 에러없이 가장을 할 수 있을 것이다.

_service.ImpersonatedUserId = new ImpersonatedUserId(ConnectingIdType.SmtpAddressaddress);

[코드1] C# 코드에서 가장하기


위 코드와 같이 가장이 완성이 되면 개인 계정에 접근하여 읽기 및 메일 발송, 일정, 그룹등 아이템 생성 및 수정을 할 수 있다.

모듈별 직접 링크 주소

(메일, 할일, 주소록, 피플)


 Exchange 2013에서 OWA 사이트가 일반 데스크탑과 타블렛, 모바일 사이트의 레이아웃을 같이 제공해 주고 있다. 데스크 탑에서 브라우저에서 layout  파라메터를 통해 타블렛과 모바일로 들어 갈 수 있는 방법을 제공해 주고 있으니 아래의 링크 주소 처럼 뒤의 파라메터를 붙여서 들어가 볼 수 있다. 한번 직접 들어가 테스트 해보기 바란다.


Desktop layout owa link list


1.      메일: https://owa.owaserver.net/owa/?modurl=0 


2.      일정: https://owa.owaserver.net/owa/?modurl=1 


3.      연락처: https://owa.owaserver.net/owa/?modurl=2 


4.      작업: https://owa.owaserver.net/owa/?modurl=3 


5.      환경설정: https://owa.owaserver.net/ecp 



Tablet layout owa link


1.      메일: https://owa.owaserver.net/owa/?modurl=0&layout=twide


2.      일정: https://owa.owaserver.net/owa/?modurl=1&layout=twide


3.      연락처: https://owa.owaserver.net/owa/?modurl=2&layout=twide


4.      작업: https://owa.owaserver.net/owa/?modurl=3&layout=twide 




Mobile layout owa link


1.      메일: https://owa.owaserver.net/owa/?modurl=0&layout=tnarrow 


2.      일정: https://owa.owaserver.net/owa/?modurl=1&layout=tnarrow 


3.      연락처: https://owa.owaserver.net/owa/?modurl=2&layout=tnarrow


4.      작업: https://owa.owaserver.net/owa/?modurl=3&layout=tnarrow 





OWA 로그인 처리

 


 아래와 같이 Exchange에서 제공하는 OWA 사이트를 다른 사이트와 연동하기 위해 SSO처리를 해야 한다면 아래와 같은 작업으로 로그인을 해줄 수 있다. OWA에 자동 로그인을 하기 위해서는 사전에 ID, PW를 알고 있어야 SSO를 할 수 있다는 제약 사항이 있다. 꼭 Exchange에서 사용하는 계정의 ID와 Password를 넘겨야 한다.


[코드1] Ajax를 이용한 로그인


 위 코드는 ajax를 통해서 로그인을 시켜주는 작업이다. 아이디와 패스워드를 '/owa/auth.owa' 에 넘겨서 로그인을 시켜준다. 이 작업을 OWA에 들어 오기 전에 작업을 해주면 사용자가 로그인 작업을 거치지 않고 곧바로 OWA창에 접속할 수 있게 된다. 아래 '코드2'는 ajax대신 submit 방식으로 로그인을 해주는 작업이다.


[코드2] Form submit를 이용한 로그인



 이렇게 OWA에 로그인을 시켜 두고 아래의 포스트에서 알려준 방법대로 레이아웃에 맞게 들어갈 수 있도록 할 수 있다.

2013/04/11 - [기타] - [Exchange] 모듈별 직접 링크 주소 - 메일, 할일, 주소록, 피플 ( modurl, layout )


Exchange의 Item에 대한 키값 - ItemId, EntryId




EntryId의 길이는 97자
 
AAAAAIHXso8JiDBMjmsY7b6fZ9UHAHZkeIKi3+5DmAT61/1J38IAAAAAAA4AAHZkeIKi3+5DmAT61/1J38IAAELCrJcAAA==
 
 
ItemId의 길이는 153자
 
AAMkAGRjNmViZjE0LTBkNDQtNGFhMi04MzhhLTEyMjJhMDEwMzI5NQBGAAAAAACzi1Zfvr+LQolz3BVmvxQYBwDOBR/ZYgNOTquQf6w91opLAAAAAAAOAADOBR/ZYgNOTquQf6w91opLAAAAAAAgAAA=
AQMkADg3ZmQyOTI0LWJiAGUzLTRkN2ItYWYzOS0yZTc4OWVhM2VmMzUARgAAA0PEo8IZrKNKkmNfq1lXCDYHALxIitm0QQJBm+jjaD8vbNkAAAMOAAAAvEiK2bRBAkGb6ONoPy9s2QAAAca/XQAAAA==
AAMkADFjZDk1MzBlLWVmMTktNDFlOC1hNjJmLTIyMTY5MmMxMTczNgBGAAAAAADKtxk8sqFjTLQeGbaRCyMGBwAkU2tjeH5cRIiV4NFkhTN8AAAAAAAOAAAkU2tjeH5cRIiV4NFkhTN8AAAAwrLdAAA=
AQMkADY4YTg1ZjI0LTU2ZTEtNDQAMmEtYWRmMi03YmM4ZTA4M2QxZjUARgAAA7S8teuglKNAhDpqazREvtMHAH0X7/D3fCxLjdnwigTKyWEAAAMOAAAAfRfv8Pd8LEuN2fCKBMrJYQAAAccFXQAAAA==
AQMkADA1MjNiZjFjLTFmNzItNGUwYy05NWY4LTIxMjQ4NTQyYzk5AGEARgAAAxhik+zM5GRCnErc6K5ciTYHAA3tOaXzvWxPkRYNczJw4w4AAAMOAAAADe05pfO9bE+RFg1zMnDjDgAAAccFXQAAAA==
AQMkADg3ZmQyOTI0LWJiAGUzLTRkN2ItYWYzOS0yZTc4OWVhM2VmMzUARgAAA0PEo8IZrKNKkmNfq1lXCDYHALxIitm0QQJBm+jjaD8vbNkAAAMOAAAAvEiK2bRBAkGb6ONoPy9s2QAAAca/ZQAAAA==
 



 ItemId와 EntryId 둘다 Unique Id 이나 EntryId는 EWS에서 정식적으로 사용하는 키값이 아니다. 그러나 다른 시스템과의 연계(길이 제한) 때문에 EntryId 값도 고려해 바야 한다. EntryId는 97자, ItemId는 153자를 차지하고 있다.


 연계시 제한이 없다면 ItemId, 길이 제한이 있다면 EntryId를 사용해야 할 것이다. 



 여분으로 MessageId는 발신자와 수신자가 같은 키값을 가지고 있다.

Notifications - Push, Pull, Stream Notification #6


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



 이제 마지막으로 남은 Pull Notification에 대해서 알아 보도록 하자. 원래 바로 이전 포스트에서 Pull까지 마치려고 하였으나 Listener의 소스가 너무 길어서 한번에 언급하기에는 다소 무리라고 판단이 되었다. 그래서 6회까지 이어지게 되었지만 너그러이 이해해 주시면 좋겠습니다. 


 본격적인 진행에 앞서서 Exchange  2013에서는 Pull Notification 관련 이벤트가 전형 발생하지 않고 있다. Powershell에서 Exchange 설정을 수정해야만 동작하는지는 확인하지 못하였지만 기본 상태에서는 동작하지 않는다. 그렇지만 Exchange 2010을 위해서 코드만이라도 살펴 보도록 하겠다. 


 Pull Notification 방식은 다른 어떤 방법보다 간단한 구조라서 사용 법이 간단하다.

PullNotificationListener pull = null;
 
for (int i = 0i < 1000i++)
{
    foreach (var member in Members.GetUsers())
    {
        try
        {
            string watermask = null;
            watermask = "AQAAAOlS/PGuZXZNiGy4pQo8RMLUKhkAAAAAAAA=";
            //watermask를 포함한 이후의 이벤트를 가져온다.
            pull = new PullNotificationListener(memberwatermask);
            Console.WriteLine(member);
            pull.Pulling();
            Thread.Sleep(10000);
        }
        catch (Exception ex) { Console.WriteLine(member + " Error " + ex.Message); }
    }
}

[코드1] Pull Notification 객체로 요청한다.


 위 코드는 PullNotificationListener을 실행 하도록 요청하는 클래스이며 "코드2"에서 실질적으로 Pulling을 진행 한다. 

/// <summary>
/// Pull Notification 이벤트 처리 클래스
/// </summary>
public class PullNotificationListener
{
    /// <summary>
    /// 사용자 email 주소
    /// </summary>
    private string address = null;
    /// <summary>
    /// Watermask 값
    /// </summary>
    private string watermark = null;
 
    /// <summary>
    /// 생성자
    /// </summary>
    /// <param name="address"></param>
    /// <param name="watermark"></param>
    public PullNotificationListener(string addressstring watermark)
    {
        this.address = address;
        this.watermark = watermark;
    }
 
    /// <summary>
    /// Pull notification을 실행한다.
    /// </summary>
    public void Pulling()
    {
        var service = EWSHelper.GetService(address);
 
        //timeOut를 10분으로 세팅 하였다.
        var pullSubscription = service.SubscribeToPullNotifications(EWSHelper.GetNotificationFolderId(), 10watermarkEWSHelper.GetNotificationEventTypes());
 
        //비 동기 실행
        //IAsyncResult result = pullSubscription.BeginGetEvents(new AsyncCallback(Subscription), pullSubscription);
        //비 동기 실행이 완료 할 때까지 대기 하도록 한다.
        //WaitHandle.WaitAll(new[] { result.AsyncWaitHandle });
 
 
        //동기 실행
        var events = pullSubscription.GetEvents();
 
        Console.WriteLine("start");
        foreach (var itemEvent in events.ItemEvents)
        {
            Console.WriteLine("{0}, {1}, {2}, watermark {3}"addressitemEvent.ItemIdDateTime.Now.ToString());
        }
        Console.WriteLine("end");
    }
 
    public void Subscription(IAsyncResult result)
    {
        Console.WriteLine("Subscription Start");
 
        //PullSubscription 변환
        var pullSubscription = result.AsyncState as PullSubscription;
 
        var events = pullSubscription.EndGetEvents(result);
            
        foreach (var itemEvent in events.ItemEvents)
        {
            Console.WriteLine("{0}, {1}, {2}, watermark {3}"addressitemEvent.ItemIdDateTime.Now.ToString());
        }
 
        //구독을 중단한다.
        //pullSubscription.Unsubscribe();
 
        Console.WriteLine("Subscription End");
    }
}

[코드2] Pulling 클래스


 코드에 대한 부가 설명을 하자면 "코드2"에서 동기 방식과 비 동기 방식으로 수행하는 코드가 작성되어 있지만 비 동기 방식은 주석처리가 되어 있고 동기 방식으로 수행 되도록 작성되었다. Exchange 2013에서 두가지 방식으로 테스트를 진행 하였으나 둘다 동작하지는 않았다. 어느 곳의 공개된 소스를 찾아 봐도 위와 같은 흐름으로 되어 있으며 더 이상 진행하지를 못하였다. ( 비 동기 실행의 코드 주석을 풀면 비 동기로 실행이 되고 있다 )


결론적으로 Exchange 2013에서는 Pull, Stream notification 방식으로 이벤트 정보를 받아서 치리해야 한다.

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에 정상적으로 수신되었다고 리턴 값을 보내는 정보)








Notifications - Push, Pull, Stream Notification #4


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



 이번에는 지난 시간에 이어 Push와 Pull에 대해서 알아 보도록 하겠습니다. Push 방식은 Pull과 Stream 방식과는 다른 방식으로 MBX에서 이벤트를 보내주고 Listener에서 받아 주는 구조로 되어 있기 때문에 구독할 수 있는 별도의 서비스가 필요 합니다. "그림1" 를 보면 3번 스텝에 해당하는 서비스로 데이터를 받아서 처리를 하는 구조입니다.

구조에 따라 CAS 서버가 Register와 Listener 모두를 같이 담당할 수도 있고 별도의 서버로 구성을 하셔도 됩니다. 서버의 스펙과 사용자 수에 따라 조율하면 되겠습니다.


[그림1] Pull notification data flow



2. Push notifications

 "코드1"은 비 동기로 Push 이벤트 구독을 신청한다. "그림1"에서의 1번과 2번 과정을 수행하는 것이다.

////비동기를 관리할 수 있게 함 List<System.Threading.Tasks.Task> tasks = new List<System.Threading.Tasks.Task>(); #region Push notification test // Create our listener // "그림1"에서 3번 수행에 필요한 Listener를 활성화 시켜 줌 _listener = new PushConsoleListener(_logger);
_listener.EventReceived += new PushConsoleListener.ListenEventHandler(_listener_EventReceived); //사용자 정보를 가져와서 Push 이벤트 등록 신청 한다. foreach (var member in Members.GetUsers()) {     var m = member;     var task = System.Threading.Tasks.Task.Factory.StartNew(() =>     {         try         {             //Push 이벤트 구독 신청             SubscriptionAdd(m);         }         catch (Exception ex)         {             Log.WriteLine(string.Format("{0}, {1}""SubscriptionAdd"ex.Message));         }     });     tasks.Add(task); } #endregion //모든 비동기 겍체가 완료가 될때까지 대기 함. System.Threading.Tasks.Task.WaitAll(tasks.ToArray());

[코드1] Push Event 구독 요청


 "코드2"번은 "그림1"에서 2번에 해당하는 작업을 수행한다.

/// <summary> /// Push 이벤트 구독 신청 /// </summary> /// <param name="address">사용자 ID - 이메일 주소</param> /// <param name="waterMark">워터 마크 - 특정 시점의 item을 가져올 수 있음</param>

public void SubscriptionAdd(string addressstring waterMark = null) {     Console.WriteLine("{0} 등록중"address);     //인증된 ExchangeService instance 가져오기     ExchangeService _service = EWSHelper.GetService();     var folderIds = new List<FolderId>()                 {                     WellKnownFolderName.Inbox                 };     var eventTypes = new List<EventType>();     //NewMail에 대해서만 이벤트를 받음 처리     eventTypes.Add(EventType.NewMail);  

//임시적으로 waterMark를 null로 할당( 이 기능으로 특정 시점의 item을 가져올 수 있음 ) //var waterMark = null;
    _service.ImpersonatedUserId = new ImpersonatedUserId(ConnectingIdType.SmtpAddressaddress); //옵션중에 frequency는 1분에 한번씩 Listener이 살아 있는지 체크하도록 하는 옵션이다. 1분부터 1440분까지 옵션을 줄 수가 있다.     PushSubscription pushSubscription = _service.SubscribeToPushNotifications(folderIdsnew Uri(_listener.URi), 1waterMarkeventTypes.ToArray());     memberAddress.TryAdd(pushSubscription.Idaddress);     Console.WriteLine(address + " push listener 등록 됨"); }

[코드2] Push 이벤트 구독 등록


 "코드1"과 "코드2"에서와 같이 MBX에 Push event 구독을 신청하였으면 이제 Listener을 할 수 있는 역할을 하는 서비스를 세팅해야 한다. ( "코드1"에서 해당 클래스를 활성화 시켜 주는 작업은 이미 수행 되었다. )


Push 이벤트는 MBX에서 XML 형식으로 Listener에 값을 넘겨 준다. 그러므로 XML에서 원하는 데이타를 추출하기 위해 XML Parser를 이용해서 작업을 하고 EventArgs를 만들어서 EventHandler를 넘겨 Event를 통해 처리하도록 하고 있다. 아래는 MBX에서 보내주는 XML 전문이다. 

<!-- 정상적인 메시지 -->
<?xml version="1.0" encoding="utf-8"?>
<soap11:Envelope xmlns:soap11="http://schemas.xmlsoap.org/soap/envelope/">
  <soap11:Header>
    <t:RequestServerVersion xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" Version="Exchange2013" xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" />
  </soap11:Header>
  <soap11:Body>
    <m:SendNotification xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages">
      <m:ResponseMessages>
        <m:SendNotificationResponseMessage ResponseClass="Success">
          <m:ResponseCode>NoError</m:ResponseCode>
          <m:Notification>
            <t:SubscriptionId>DwBtbWJ4MDIuYnBpby5uZXQQAAAA8D/AEWNefUirlcJW7vLT2zE/ABKs3s8I</t:SubscriptionId>
            <t:PreviousWatermark>AQAAAKGzfQd17kZIovKBge0oWhrMcAAAAAAAAAE=</t:PreviousWatermark>
            <t:MoreEvents>false</t:MoreEvents>
            <t:StatusEvent>
              <t:Watermark>AQAAAKGzfQd17kZIovKBge0oWhrMcAAAAAAAAAE=</t:Watermark>
            </t:StatusEvent>
          </m:Notification>
        </m:SendNotificationResponseMessage>
      </m:ResponseMessages>
    </m:SendNotification>
  </soap11:Body>
</soap11:Envelope>

[코드3] MBX가 Listener에게 보내주는 XML 정보 (정상)


<!-- 비 정상적인 메시지 -->
<?xml version="1.0" encoding="utf-8"?>
<soap11:Envelope xmlns:soap11="http://schemas.xmlsoap.org/soap/envelope/">
  <soap11:Header>
    <t:RequestServerVersion xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" Version="Exchange2013" xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" />
  </soap11:Header>
  <soap11:Body>
    <m:SendNotification xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages">
      <m:ResponseMessages>
        <m:SendNotificationResponseMessage ResponseClass="Error">
          <m:MessageText>이 구독의 이벤트를 검색할 수 없습니다. 구독을 다시 만들어야 합니다.</m:MessageText>
          <m:ResponseCode>ErrorReadEventsFailed</m:ResponseCode>
          <m:DescriptiveLinkKey>0</m:DescriptiveLinkKey>
          <m:MessageXml>
            <t:Value Name="SubscriptionId">DwBtbWJ4MDEuYnBpby5uZXQQAAAAPxYnRYDXgkCvXyhllmZHPpV2k0Sr3s8I</t:Value>
          </m:MessageXml>
        </m:SendNotificationResponseMessage>
      </m:ResponseMessages>
    </m:SendNotification>
  </soap11:Body>
</soap11:Envelope>

[코드4]MBX가 Listener에게 보내주는 XML 정보 (비 정상)


 이제 XML 파일을 파싱해서 얻어진 정보를 EventArgs로 사용하는 클래스를 먼저 살펴 보자. 이 클래스를 통해 Listener에 연결된 핸들러에서 편리하게 해당 정보를 얻어 사용할 수 있다. 

/// <summary>
/// EWS 2.0, Exchange 2013, Push notification 관련 XML에 관계된 EventArgs
/// </summary>
public class ListenEventArgs : EventArgs
{
    #region fields
    private string _xml = "";
    private string _subscriptionId = "";
    private EventType _eventtype = EventType.Status;
    private ItemId _itemId = new ItemId(new Guid().ToString());
    private FolderId _folderId = null;
    private FolderId _parentFolderId = null;
    private ItemId _oldItemId = null;
    private FolderId _oldFolderId = null;
    private FolderId _oldParentFolderId = null;
    #endregion
 
    #region Constructor
    /// <summary>
    /// XML 데이터를 가지고 인스턴스 생성
    /// </summary>
    /// <param name="XML">XML 데이타</param>
    public ListenEventArgs(string XML)
    {
        //XML에 notification 상세 정보가 있다.
        _xml = XML;
    }
 
    /// <summary>
    /// 인스턴스 생성
    /// </summary>
    /// <param name="SubscriptionId">구독 요청시 생성된 ID값</param>
    /// <param name="isError">에러여부</param>
    public ListenEventArgs(string SubscriptionIdbool isError)
    {
        this._subscriptionId = SubscriptionId;
        this.IsError = isError;
    }
 
    /// <summary>
    /// 인스턴스 생성
    /// </summary>
    /// <param name="SubscriptionId">구독 요청시 생성된 ID값</param>
    /// <param name="EventType">이벤트 타입</param>
    /// <param name="ItemId">ItemId</param>
    /// <param name="ParentFolderId">ParentFolderId</param>
    public ListenEventArgs(string SubscriptionIdEventType EventTypeItemId ItemIdFolderId ParentFolderId)
    {
        _subscriptionId = SubscriptionId;
        _eventtype = EventType;
        _itemId = ItemId;
        _parentFolderId = ParentFolderId;
    }
 
    /// <summary>
    /// 인스턴스 생성
    /// </summary>
    /// <param name="SubscriptionId"></param>
    /// <param name="EventType"></param>
    /// <param name="ItemId"></param>
    /// <param name="ParentFolderId"></param>
    /// <param name="OldItemId"></param>
    /// <param name="OldParentFolderId"></param>
    public ListenEventArgs(string SubscriptionIdEventType EventTypeItemId ItemIdFolderId ParentFolderIdItemId OldItemIdFolderId OldParentFolderId)
        : this(SubscriptionIdEventTypeItemIdParentFolderId)
    {
        _oldItemId = OldItemId;
        _oldParentFolderId = OldParentFolderId;
    }
 
    /// <summary>
    /// 인스턴스 생성
    /// </summary>
    /// <param name="SubscriptionId"></param>
    /// <param name="EventType"></param>
    /// <param name="FolderId"></param>
    /// <param name="ParentFolderId"></param>
    public ListenEventArgs(string SubscriptionIdEventType EventTypeFolderId FolderIdFolderId ParentFolderId)
    {
        _subscriptionId = SubscriptionId;
        _eventtype = EventType;
        _folderId = FolderId;
        _parentFolderId = ParentFolderId;
    }
 
    /// <summary>
    /// 인스턴스 생성
    /// </summary>
    /// <param name="SubscriptionId"></param>
    /// <param name="EventType"></param>
    /// <param name="FolderId"></param>
    /// <param name="ParentFolderId"></param>
    /// <param name="OldFolderId"></param>
    /// <param name="OldParentFolderId"></param>
    public ListenEventArgs(string SubscriptionIdEventType EventTypeFolderId FolderIdFolderId ParentFolderIdFolderId OldFolderIdFolderId OldParentFolderId)
        : this(SubscriptionIdEventTypeFolderIdParentFolderId)
    {
        _oldFolderId = OldFolderId;
        _oldParentFolderId = OldParentFolderId;
    }
    #endregion
 
    /// <summary>
    /// Push 구독 요청시 반환된 값
    /// </summary>
    public string SubscriptionId
    {
        get { return _subscriptionId; }
    }
 
    /// <summary>
    /// 이벤트 타입
    /// </summary>
    public EventType EventType
    {
        get { return _eventtype; }
    }
 
    /// <summary>
    /// Item unique Id
    /// </summary>
    public ItemId ItemId
    {
        get { return _itemId; }
    }
 
    /// <summary>
    /// Folder Id
    /// </summary>
    public FolderId FolderId
    {
        get { return _folderId; }
    }
 
    /// <summary>
    /// Parent folder Id
    /// </summary>
    public FolderId ParentFolderId
    {
        get { return _parentFolderId; }
    }
 
    /// <summary>
    /// Old Item Id
    /// </summary>
    public ItemId OldItemId
    {
        get { return _oldItemId; }
    }
 
    /// <summary>
    /// Old Parent Folder Id
    /// </summary>
    public FolderId OldParentFolderId
    {
        get { return _oldParentFolderId; }
    }
 
    /// <summary>
    /// MBX 주소
    /// </summary>
    public string RemortEndpoint { getset; }
 
    /// <summary>
    /// 워터마크
    /// </summary>
    public string Watermark { getset; }
 
    /// <summary>
    /// 이전 워터마크
    /// </summary>
    public string PreviousWatermark { getset; }
 
    /// <summary>
    /// 에러여부
    /// </summary>
    private bool isError = false;
 
    /// <summary>
    /// 에러여부
    /// </summary>
    public bool IsError
    {
        get
        {
            return isError;
        }
        set
        {
            isError = value;
        }
    }
 
    /// <summary>
    /// Push server에서 보낸 준 XML Data
    /// </summary>
    public string Xml
    {
        get { return _xml; }
        set { _xml = value; }
    }
}

[코드5] XML에서 파싱된 정보를 저장하는 클래스


 다음 포스트에서 Listener에서 XML을 파싱하는 클래스부터 다뤄보도록 하겠다.

+ Recent posts