[Exchange] Exchange의 EWS에서 일정관련 연계 작업시 GlobalObjectId를 이용해 일정 찾기


일정을 사용자가 작성하면 ItemId와 GlobalObjectId가 생성이 된다. 

ItemId는 Exchange에서 Item을 인식하는 Unique 값으로 유일한 값을 나타낸다. 그리고 다른 사용자와의 일정에서 공통적인 값을 GlobalObjectId를 사용하여 같은 일정인지를 알아 낼 수 있다.


사용자A가 일정을 만들 때 사용자B를 참조인으로 입력 후 생성하면


사용자A : ItemId : aa1, GlobalObjectId : abcdef

사용자B : ItemId : bb1, GlobalObjectId : abcdef


와 같이 생성이 되게 된다.



그러면 타 시스템에서 EWS를 사용하여 정보를 알아 내기 위해서 아래와 같은 코드로 GlobalObjectId를 가져올 수 있다.


Appointment exItem = null;
 
// GlobalObjectId
ExtendedPropertyDefinition globalObjectId = new ExtendedPropertyDefinition(DefaultExtendedPropertySet.Meeting0x03MapiPropertyType.Binary);
 
//하나의 객체에 대해서만 바인딩
exItem = Item.Bind(service.Servicenotification.ItemIdnew PropertySet(
            ItemSchema.Id,
            ItemSchema.Subject,
            AppointmentSchema.ICalUid,
            globalObjectId)) as Appointment;

[코드1] GlobalObjectId의 값을 가져오는 코드


위 코드에서 ExtendedPropertyDefinition으로 확장 프로퍼티를 사용해서 가져와야 하며 선언 시 '0x03'을 입력하고 Binary를 선택해야 한다. 이제 넘겨진 값을 '코드2'에서와 같이 스트링으로 변환해서 사용 할 수 있다.


Convert.ToBase64String((byte[])exItem.ExtendedProperties[0].Value);

[코드2] Binary 형식을 스트링으로 변환하는 코드



이와 같은 코드로 다른 시스템과 연계하여 여러사람에 관계된 같은 일정을 찾아서 변경 해 줄 수 있을 것이다. 그렇지만 예외 케이스가 있다. 반복 일정에서 특정 항목만 편집하여 참조인을 추가 하는 케이스에서는 GlobalObjectId가 변경이 일어 나게 된다. 아래 글에서 설명이 나와 있다.



Using GlobalObjectId poses a problem however…If you were to create a recurring appointment and invite one attendee for only one instance of that recurring appointment that attendee ends up with only the one exception instance of the parent recurring appointment in their calendar.  This is called an “orphaned” appointment.  This type of appointment’s GlobalObjectId has additional information in it related to the exception date (for more information look at PidLidGlobalObjectId in MS-OXOCAL).  The bottom line here is that this orphaned instance in this one attendee’s calendar will have a GlobalObjectId, and therefore UID, which doesn’t match the related appointments in the other attendee’s and organizer’s calendar.  Henning’s code won’t work in this specific scenario…


참조 URL : http://blogs.msdn.com/b/mstehle/archive/2009/09/02/ews-uid-not-always-the-same-for-orphaned-instances-of-the-same-meeting.aspx


 


그래서 위의 문제점과 관련하여 CleanGlobalObjectId 라는 것이 존재하고 이는 위의 같은 CASE일때도 Id가 달라 지지 않는 프로퍼티를 사용해야 할 것이다.


The format of this property is the same as that of LID_GLOBAL_OBJID (PidLidGlobalObjectId). The value of this property must be equal to the value of LID_GLOBAL_OBJID, except the YH, YL, M, and D fields must be zero. All objects that refer to an Instance of a recurring series (including an orphan instance), as well as the recurring series itself, will have the same value for this property.


참고 URL : http://msdn.microsoft.com/en-us/library/cc839502.aspx



그래서 결론 적으로 아래 코드처럼 ClearnGlobalObjectId를 사용해서 값을 가져와서 연계 하면 변하지 않는 값으로 외부 시스템과 연동할 수 있을 것이다.


// GlobalObjectId
// ExtendedPropertyDefinition globalObjectId = new ExtendedPropertyDefinition(DefaultExtendedPropertySet.Meeting, 0x03, MapiPropertyType.Binary);
 
// LID_GLOBAL_OBJID 
ExtendedPropertyDefinition globalObjectId = new ExtendedPropertyDefinition(DefaultExtendedPropertySet.Meeting0x23MapiPropertyType.Binary);

[코드3] ClearnGlobalObjectId로 변경한 코드








일정에 따르는 내부 이벤트 정보

( Item, GlobalObjectId 추적 )



 반복 일정에서 ItemId와 GlobalObjectId의 상관 관계 그리고 수정에 대해서 알아 보고자 한다. 일정 관련 개발시 또는 연동 때문에 여러가지 케이스에 대해서 밑에와 같이 정리해 보았다. 익스체인지 일정은 과거 일정과 미래 일정이 현재 일을 포함 할 경우 이전 일정에 대해서는 새로 생성을 하고 기존 일정을 수정하여 처리하고 있다. 이렇게 변경되는 사항들을 잘 정리해서 쉽게 일정 연동을 할 수 있기를 바라겠다.



밑에서 나온 분석 자료에서 'Uid'가 익스체인지의 ItemId임을 주지 하기 바랍니다.



1. 현재일 보다 미래 일정에 대해서 수정
 
8월 12일 반복 시작일

1.1  화요일 반복 일정 생성
{"Uid":"AAMkAGM2NmM0MDFhLTMxMDItNGEzNC04MjQ5LTdmNTcyMmNlYmY0MgBGAAAAAAD14fnht2jpSpaypawvJwO3BwA2K3ZCpui2TKCftY0IT00tAAAAAAAPAAA2K3ZCpui2TKCftY0IT00tAAA2tpB3AAA=","EMail":"test@test.com","Tstamp":"635116401739714845","Title":"반복 테스트 #1 화요일","GlobalObjectId":"BAAAAIIA4AB0xbcQGoLgCAAAAABqFxwHnpTOAQAAAAAAAAAAEAAAAErmMOMEWQpJoohCYDFvDhU=","EventType":"Created"}

1.2 화요일 반복 일정 생성 전체 일정 수정
{"Uid":"AAMkAGM2NmM0MDFhLTMxMDItNGEzNC04MjQ5LTdmNTcyMmNlYmY0MgBGAAAAAAD14fnht2jpSpaypawvJwO3BwA2K3ZCpui2TKCftY0IT00tAAAAAAAPAAA2K3ZCpui2TKCftY0IT00tAAA2tpB3AAA=","EMail":"test@test.com","Tstamp":"635116403135292644","Title":"반복 테스트 #1 화요일","GlobalObjectId":"BAAAAIIA4AB0xbcQGoLgCAAAAABqFxwHnpTOAQAAAAAAAAAAEAAAAErmMOMEWQpJoohCYDFvDhU=","EventType":"Modified"}

1.3 화요일 반복 일정 한 항목만 수정 - 날짜 수정 없음
{"Uid":"AAMkAGM2NmM0MDFhLTMxMDItNGEzNC04MjQ5LTdmNTcyMmNlYmY0MgBGAAAAAAD14fnht2jpSpaypawvJwO3BwA2K3ZCpui2TKCftY0IT00tAAAAAAAPAAA2K3ZCpui2TKCftY0IT00tAAA2tpB3AAA=","EMail":"test@test.com","Tstamp":"635116432690946758","Title":"반복 테스트 #1 화요일","GlobalObjectId":"BAAAAIIA4AB0xbcQGoLgCAAAAABqFxwHnpTOAQAAAAAAAAAAEAAAAErmMOMEWQpJoohCYDFvDhU=","EventType":"Modified"}

1.4 화요일 반복 일정 한 항목만 수정 - 날짜 수정 있음
{"Uid":"AAMkAGM2NmM0MDFhLTMxMDItNGEzNC04MjQ5LTdmNTcyMmNlYmY0MgBGAAAAAAD14fnht2jpSpaypawvJwO3BwA2K3ZCpui2TKCftY0IT00tAAAAAAAPAAA2K3ZCpui2TKCftY0IT00tAAA2tpB3AAA=","EMail":"test@test.com","Tstamp":"635116433589856729","Title":"반복 테스트 #1 화요일","GlobalObjectId":"BAAAAIIA4AB0xbcQGoLgCAAAAABqFxwHnpTOAQAAAAAAAAAAEAAAAErmMOMEWQpJoohCYDFvDhU=","EventType":"Modified"}

1.5 화요일 반복 일정 - 모든 반복 일정 삭제
{"Uid":"AAMkAGM2NmM0MDFhLTMxMDItNGEzNC04MjQ5LTdmNTcyMmNlYmY0MgBGAAAAAAD14fnht2jpSpaypawvJwO3BwA2K3ZCpui2TKCftY0IT00tAAAAAAAPAAA2K3ZCpui2TKCftY0IT00tAAA2tpB3AAA=","EMail":"test@test.com","Tstamp":null,"Title":null,"GlobalObjectId":null,"EventType":"Deleted"}
 
1.6 반복 일정중에서 한 항목에 대해서만 삭제
2013-08-09 11:29:29,239 [ 116] INFO Default - SonicMQ send : {"Uid":"AAMkAGM2NmM0MDFhLTMxMDItNGEzNC04MjQ5LTdmNTcyMmNlYmY0MgBGAAAAAAD14fnht2jpSpaypawvJwO3BwA2K3ZCpui2TKCftY0IT00tAAAAAAAPAAA2K3ZCpui2TKCftY0IT00tAAA2tpBzAAA=","EMail":"test@test.com","Tstamp":"635116445692231978","Title":"토요 반복 일정","GlobalObjectId":"BAAAAIIA4AB0xbcQGoLgCAAAAADuh2SPJZTOAQAAAAAAAAAAEAAAAK8400lwUDdGhrjVUfWxnmQ=","EventType":"Modified"}
2013-08-09 11:29:34,066 [ 116] INFO Default - SonicMQ send : {"Uid":"AAMkAGM2NmM0MDFhLTMxMDItNGEzNC04MjQ5LTdmNTcyMmNlYmY0MgBGAAAAAAD14fnht2jpSpaypawvJwO3BwA2K3ZCpui2TKCftY0IT00tAAAAAAAPAAA2K3ZCpui2TKCftY0IT00tAAA2tpBzAAA=","EMail":"test@test.com","Tstamp":"635116445740518419","Title":"토요 반복 일정","GlobalObjectId":"BAAAAIIA4AB0xbcQGoLgCAAAAADuh2SPJZTOAQAAAAAAAAAAEAAAAK8400lwUDdGhrjVUfWxnmQ=","EventType":"Modified"}
2013-08-09 11:29:44,145 [ 105] INFO Default - SonicMQ send : {"Uid":"AAMkAGM2NmM0MDFhLTMxMDItNGEzNC04MjQ5LTdmNTcyMmNlYmY0MgBGAAAAAAD14fnht2jpSpaypawvJwO3BwA2K3ZCpui2TKCftY0IT00tAAAAAAAPAAA2K3ZCpui2TKCftY0IT00tAAA2tpBzAAA=","EMail":"test@test.com","Tstamp":"635116445840891818","Title":"토요 반복 일정","GlobalObjectId":"BAAAAIIA4AB0xbcQGoLgCAAAAADuh2SPJZTOAQAAAAAAAAAAEAAAAK8400lwUDdGhrjVUfWxnmQ=","EventType":"Modified"}
 
1.7 반복일정의 최종 삭제
2013-08-09 11:29:49,043 [ 133] INFO Default - SonicMQ send : {"Uid":"AAMkAGM2NmM0MDFhLTMxMDItNGEzNC04MjQ5LTdmNTcyMmNlYmY0MgBGAAAAAAD14fnht2jpSpaypawvJwO3BwA2K3ZCpui2TKCftY0IT00tAAAAAAAPAAA2K3ZCpui2TKCftY0IT00tAAA2tpBzAAA=","EMail":"test@test.com","Tstamp":null,"Title":null,"GlobalObjectId":null,"EventType":"Deleted"}
 
 
 금일(오늘)을 포함하지 않는 미래의 일정은 ItemId와 GlobalObjectId가 변경되지 않고 예상되로 반복 일정 수정이 일어 나고 있다. 



 
2. 과거일에서 현재일 포함 미래일 범위의 일정에 대해서 수정
 
 
8월 1일 반복 시작일

2.1 과거 목요일 반복 일정 생성
{"Uid":"AAMkAGM2NmM0MDFhLTMxMDItNGEzNC04MjQ5LTdmNTcyMmNlYmY0MgBGAAAAAAD14fnht2jpSpaypawvJwO3BwA2K3ZCpui2TKCftY0IT00tAAAAAAAPAAA2K3ZCpui2TKCftY0IT00tAAA2tpCDAAA=","EMail":"test@test.com","Tstamp":"635116536408209382","Title":"반복일정 테스트 #3 - 목요일","GlobalObjectId":"BAAAAIIA4AB0xbcQGoLgCAAAAADIsGZkvZTOAQAAAAAAAAAAEAAAAL7jcbkKvu5CkiLigoQ5+PI=","EventType":"Created"}

2.2 과거 목요일 반복 일정 변경 - 범위만 변경
{"Uid":"AAMkAGM2NmM0MDFhLTMxMDItNGEzNC04MjQ5LTdmNTcyMmNlYmY0MgBGAAAAAAD14fnht2jpSpaypawvJwO3BwA2K3ZCpui2TKCftY0IT00tAAAAAAAPAAA2K3ZCpui2TKCftY0IT00tAAA2tpCDAAA=","EMail":"test@test.com","Tstamp":"635116536652862034","Title":"반복일정 테스트 #3 - 목요일","GlobalObjectId":"BAAAAIIA4AB0xbcQGoLgCAAAAADIsGZkvZTOAQAAAAAAAAAAEAAAAL7jcbkKvu5CkiLigoQ5+PI=","EventType":"Modified"}
 
2.3 과거 목요일 반복 일정 변경 - 반복일을 금요일로 날짜 변경

2.3.1 과거 목요일 반복 일정 변경 - 금요일로 날짜 변경 - 8월 9일 이전의 일정이 새로 생성 됨
{"Uid":"AAMkAGM2NmM0MDFhLTMxMDItNGEzNC04MjQ5LTdmNTcyMmNlYmY0MgBGAAAAAAD14fnht2jpSpaypawvJwO3BwA2K3ZCpui2TKCftY0IT00tAAAAAAAPAAA2K3ZCpui2TKCftY0IT00tAAA2tpCEAAA=","EMail":"test@test.com","Tstamp":"635116537102912102","Title":"반복일정 테스트 #3 - 목요일","GlobalObjectId":"BAAAAIIA4AB0xbcQGoLgCAAAAADIsGZkvZTOAQAAAAAAAAAAEAAAAL7jcbkKvu5CkiLigoQ5+PI=","EventType":"Created"}

2.3.2 과거 목요일 반복 일정 변경 - 금요일로 날짜 변경 - 8월 9일 이후의 일정만 수정 됨{"Uid":"AAMkAGM2NmM0MDFhLTMxMDItNGEzNC04MjQ5LTdmNTcyMmNlYmY0MgBGAAAAAAD14fnht2jpSpaypawvJwO3BwA2K3ZCpui2TKCftY0IT00tAAAAAAAPAAA2K3ZCpui2TKCftY0IT00tAAA2tpCDAAA=","EMail":"test@test.com","Tstamp":"635116537103422167","Title":"반복일정 테스트 #3 - 목요일","GlobalObjectId":"BAAAAIIA4AB0xbcQGoLgCAAAAAAouLOLvZTOAQAAAAAAAAAAEAAAAKD44Bwh7SFNoqxBurnSKYk=","EventType":"Modified"}
 
 
과거일 포함 수정이 일어나면
 - 이전 날짜는 새로 생성이 되고 GlobalObjectID는 이전과 동일 합니다. ( 새로 생성이 되어 ItemID는 다름 )
 - 이때 이후 날짜는 수정이 되며 ItemId는 같지만 GlobalObjectID는 다르게 변경 됨
 
 
 
2.4 목요일 반복 일정 전체 삭제 - 8월 9일 이전일 삭제 진행 - 삭제시 Exchange에 Item이 없기 때문에 GlobalObjectID를 알아 낼 수 없음
{"Uid":"AAMkAGM2NmM0MDFhLTMxMDItNGEzNC04MjQ5LTdmNTcyMmNlYmY0MgBGAAAAAAD14fnht2jpSpaypawvJwO3BwA2K3ZCpui2TKCftY0IT00tAAAAAAAPAAA2K3ZCpui2TKCftY0IT00tAAA2tpCEAAA=","EMail":"test@test.com","Tstamp":null,"Title":null,"GlobalObjectId":null,"EventType":"Deleted"}
 
2. 5 금요일 반복 일정 전체 삭제 - 8월 9일 이후일 삭제 진행 - 삭제시 Exchange에 Item이 없기 때문에 GlobalObjectID를 알아 낼 수 없음
{"Uid":"AAMkAGM2NmM0MDFhLTMxMDItNGEzNC04MjQ5LTdmNTcyMmNlYmY0MgBGAAAAAAD14fnht2jpSpaypawvJwO3BwA2K3ZCpui2TKCftY0IT00tAAAAAAAPAAA2K3ZCpui2TKCftY0IT00tAAA2tpCDAAA=","EMail":"test@test.com","Tstamp":null,"Title":null,"GlobalObjectId":null,"EventType":"Deleted"}
 
 
 
 
-- 새로 일정 생성해서 진행 함 
 
2.6 금일 이전 한 항목만 일자 변경은 저장을 할 수 없어서 수행 하지 못함
 
2.7 금일 이후 한 항목만 일자 변경
{"Uid":"AAMkAGM2NmM0MDFhLTMxMDItNGEzNC04MjQ5LTdmNTcyMmNlYmY0MgBGAAAAAAD14fnht2jpSpaypawvJwO3BwA2K3ZCpui2TKCftY0IT00tAAAAAAAPAAA2K3ZCpui2TKCftY0IT00tAAA2tpCFAAA=","EMail":"test@test.com","Tstamp":"635116547854317040","Title":"반복일정 테스트 #4 - 목요일","GlobalObjectId":"BAAAAIIA4AB0xbcQGoLgCAAAAADVpTb8v5TOAQAAAAAAAAAAEAAAAFO0GqAx2KxCu43J1ILBwSw=","EventType":"Modified"}




 

 금일(오늘)을 포함한 반복 일정은 ItemId와 GlobalObjectId가 변경된다. 금일 이전 일정은 새롭게 생성되어 ItemId가 생성되지만 GlobalObjectId는 동일한다. 그러지만 수정된 반복 일정은 ItemId는 동일하지만 GlobalObjectId가 변경된다. 이 규칙을 잘 이해해야 익스체인지의 일정과 연계 개발을 쉽게 할 수 있을 것이다. 









동적 사용자의 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 하는 방법은 다음 포스트에서 이어 가도록 하겠다.





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








+ Recent posts