MsExchange piekļuve caur MAPI

Tāds kā ievads

Savajadzējās pašam “notēlot Outlook-u”, lai lasītu vēstules. Tādēļ nācās iegūt nelielu pieredzi darbā ar MAPI, konkrētāk, ar tā “frontendu” MS CDO 1.2.1., lai piekļūtu Microsoft Exchange server direktorijām un “lasītu” e-pasta vēstules. Par to tad arī uzrakstīšu.

Nosaukumu murgs

Jāsāk ar to, ka Microsoft ir krietni pacentušies, lai sajauktu nomenklatūru šajos jautājumos.

Termins MAPI ir atšifrējams kā Messaging API, kuru nodrošina MAPI subsistēma. MAPI aptver daudz plašākas iespējas kā tikai e-pasta sistēmu veidošana, tas ir dzinējs gandrīz jebkāda veida ziņojumapmaiņai (skatīt aprakstu par subsistēmu).

Savukārt CDO jeb Collaboration Data Objects, kas sākotnēji saucies kā “OLE messaging” un mazliet vēlāk “Active messaging”, ir MAPI implementācija, kas paredzēta, lai no klientaplikācijām varētu pieslēgties MAPI serverim. Taču CDO neatbalsta pilno MAPI implementāciju, bet tikai daļu no tās. Tādēļ Microsoft apgalvo, ka CDO implementē “Collaboration Data Objects API specification” otro versiju.

Bez šiem diviem terminiem parādās vēl arī trešais – Simple MAPI, kas ļauj no klientaplikācijām veikt pavisam vienkāršas darbības ar MAPI serveri. To šobrīd neapskatīsim. Mazliet precīzāk par atšķirībām starp MAPI, CDO, SimpleMAPI Microsoft paši skaidro šeit.

Kas kur atrodams

Nu, lūk. Tātad ir mums tāda bibliotēka MAPI32.dll, kas atrodas %windows%\system32 katalogā. Tā nu nemaz nav pareizā bibliotēka (ak jā, te vēl jāpiebilst, ka mērķa valoda, kurā jāveido mana programma, ir MS Visual Basic .net) – ar šo bibliotēku var programmēt tie, kas pārvalda C/C++. Cik saprotu, MAPI objekti neatbalsta IDispatch interfeisu, līdz ar to nav piemērojami valodām, kuras izmanto late-binding. Piemēram, skriptu valodām vai VB.Net. Vēl turpat system32 ir arī bibliotēka cdo32.dll – arī tā nav “pareizā” – tā ir Crystal Data Objects, pavisam cita bibliotēka. Turpat system32 atrodas arī vēl viena bibliotēka ar uzvedinošu nosaukumu cdosys.dll – tā paredzēta SMTP un tamlīdzīgām lietām, vismaz tā skaidro Microsoft. “Pareizais” fails ir meklējams takā C:\Program Files\Common Files\System\MSMAPI\1033. Ja jautāsiet, vai šis fails tur atrodas, ja datorā instalēts “pliks” Windows, atbildēšu, ka nē, neatrodas vis. CDO tiek instalēts tikai tad, ja uz datora instalēts MS Outlook (pilnais, nevis Outlook Express, ja nepieciešama piekļuve Ms Exchange serverim) vai arī pats MS Exchange server.

Interop asamblejas

Lai šo bibliotēku varētu izmantot VB.net projektā, nepieciešams izveidot tai Interop asambleju (man bija nepieciešams parakstīt manis veidoto programmu, tādēļ visām referencētajām bibliotēkām arī jābūt parakstītām). To darām šādi:

rem aizejam uz MAPI direktoriju
C:>cd C:\Program Files\Common Files\System\MSMAPI\1033
rem izveidojam savu atslēgas failu, ja nu tāda vēl nav
C:\Progra~1\Commo~1\System\MSMAPI\1033>sn -k snk.key
rem ģenerējam interop asambleju
C:\Progra~1\Commo~1\System\System\MSMAPI\1033>tlbimp cdo.dll /keyfile:snk.key /out:interop.cdo.dll

Šeit gan tiek pieņemts, ka uz datora ir instalēts .NET SDK un taka C:\Program Files\Microsoft Visual Studio .NET 2003\SDK\v1.1\Bin ir iekļauta PATH.

Kad nu ir izdevies sagatavot interop asambleju, var sākt kodēt. Veidojam jaunu VB.net projektu, liekam referencēs šo interop.cdo.dll, rakstām kodu. Protams, kodu, kas darbosies tikai uz datoriem, kur uzstādīts MS Outlook.

Sesijas veidošana

Pirmā lieta, kas jādara, lai varētu strādāt gandrīz jebkurā klienta-servera vidē, ir ielogošanās. Ar to arī sāksim. Exchange konekciju veidojam šādi:

Dim oSession As New CDO.Session
Dim sServerName As String = "server"
Dim sProfileName As String = "user"
oSession.Logon(NewSession:=True, NoMail:=False, ProfileInfo:=sServerName & vbLf & sProfileName)

Šeit fonā notiek NTLM autorizācija, kuras rezultātā tiek uzjautāta arī lietotāja parole un pārējās lietas. Par to manā gadījumā nebija jāuztraucas. Interesanti ir tas, ka nepareizi norādīts profils vai servera vārds nenozīmē, ka šis koda gabals izpildīsies ar kļūdām. Laba pārbaude ir tests, vai CurrentUser atribūts sesijai atšķiras no Nothing.

Public folders meklēšana

Kas tālāk? Man bija nepieciešams piekļūt nevis paša lietotāja Inbox folderim, bet folderiem no “Public folders” zara. Tātad, meklējam, kur ir publiskie folderi. Sākums jau ir pavisam vienkāršs:

Dim oInfoStores As CDO.InfoStores 'InfoStoru kolekcija, ko dabū no sesijas objekta
Dim oInfoStore, otInfoStore As CDO.InfoStore 'InfoStore objekts, kur atrodam "Public Folders"

'šeit dabūjam visas InfoStores, kas zināmas lietotāja sesijai 'tur būs "Inbox", "Public Folders" un varbūt vēl kāds oInfoStores = CType(oSession.InfoStores, CDO.InfoStores)

'folderu kolekcijā meklējam "Public Folders" 'jāņem vērā 1-bāzētās kolekcijas!!! For i As Integer = 1 To CInt(oInfoStores.Count) otInfoStore = oInfoStores.Item(i) If CStr(otInfoStore.Name) = "Public Folders" Then oInfoStore = CType(oInfoStores.Item(i), CDO.InfoStore) '"Public folders" ir atrasts, oInfoStore satur referenci uz to Exit For End If Next

Tomēr tas, ko esam atraduši, NAV tas, kas mums vajadzīgs. Mums ir vajadzīgs CDO.Folder tipa objekts, bet atraduši esam CDO.InfoStore objektu. Tāpēc no InfoStore objekta atribūtiem jāatrod foldera ID un jādabū pats foldera objekts. To darām šādi:

Dim oFields As CDO.Fields
Dim oFieldsItem As CDO.Field
Const PRIPMPUBLICFOLDERSENTRYID As Integer = &H66310102
'noskaidrojam visus public folders atribūtus
oFields = CType(oInfoStore.Fields, MAPI.Fields)
'atrodam atribūtos Public folders ID
oFieldsItem = CType(oFields.Item(PRIPMPUBLICFOLDERSENTRYID), MAPI.Field)

'dabū referenci uz Public Folders FOLDERA (nevis InfoStore) objektu 'te jālieto GetFolder pēc foldera ID, citādi notiek kļūda. oFolder = CType(ocMapiSession.Session.GetFolder(oFieldsItem.Value, oInfoStore.ID), MAPI.Folder)

Ja viss būtu bijis normāli, es būtu varējis šo jocīgo koda gabalu aizvietot ar vienkāršu oFolder=oInfoStore.RootFolder. Taču nē. Kā microsoft raksta šeit, “If your application is running as a Microsoft Windows NT® service, you cannot access the Microsoft Exchange Public Folders through the normal hierarchy because of a notification conflict.”, tas ir, ja kods darbojas kā Windows serviss (kas arī tieši mans gadījums), tad publiskajiem folderiem nevar piekļūt “pa tiešo”.

Apakšfoldera atrašana

Pēc tam atliek publiskajos folderos sameklēt nepieciešamo apakšfolderi. Arī mazliet netriviāli, jo apakšfolderu kolekcija nav pieejama pēc nosaukumiem, bet tikai apstaigājama pēc kārtas numuriem. Pieņemsim, ka gribu piekļūt pie “Public Folders/All Public folders/Internet Newsgroups/alt/2600”

Dim strFolderi() As String = {"All public folders", "Internet Newsgroups", "alt", "2600hz"}
oFolders = CType(oFolder.Folders, CDO.Folders)
For Each sFolder As String In strFolderi
    For iSk As Integer = 1 To CInt(oFolders.Count)
          'iterē cauri objektiem darba folderī, 
          'līdz atrod nākamo līmeni
          oFolder = CType(oFolders.Item(iSk), CDO.Folder)
           If CStr(oFolder.Name) = sFolder Then
              'ja vārds sakrīt, tad varam iet vienu līmeni zemāk
              oFolders = CType(oFolder.Folders, CDO.Folders)
              Exit For
          End If
      Next
Next

Ziņojumu lasīšana

Tā, tagad nu gan esam atraduši vajadzīgo taku. Sāksim lasīt ziņojumus. Pirmkārt jau, negribas kārtējo reizi pārlasīt visus ziņojumus. Filtrēsim tikai nelasītos.

Dim oMessages As CDO.Messages
Dim oMessage As CDO.Message
Dim oFilter As CDO.MessageFilter

oMessages = CType(oFolder.Messages, CDO.Messages) oFilter = CType(oMessages.Filter, CDO.MessageFilter) oFilter.Unread = True For iCnt As Integer = CInt(oMessages.Count) To 1 Step -1 oMessage = CType(oMessages.Item(iCnt), CDO.Message) Debug.WriteLine(CStr(oMessage.Subject)) Next

Kodam izpildoties, debug logā tiek izdrukātas rindas ar atrasto e-pasta vēstuļu nosaukumiem. Jaukāk gan faktiski būtu izdrukāt arī sūtītāju vārdus. Pieliekam papildus koda rindu:

Debug.Write(CStr(CType(oMessage.Sender, CDO.AddressEntry).Address))

Outlook security patch

Un tagad sākas… Diemžēl šis kods vairs neizpildīsies, ja vien būs instalētas relatīvi jaunas MS Exchange server un Outlook versijas. Kāpēc? Pirms dažiem gadiem populāri kļuva dažādi e-pasta vīrusi. Vēstulēm tika pielikti VBS attachmenti un, tos izpildot, tie darīja savu slikto darbu. Sākotnēji dažādu MS Outlook nekorektas izpildes dēļ šie VBS faili izpildījās paši, tikai PASKATOTIES uz tiem caur “preview pane”, vēlāk Outlook tika labots un atradās citi veidi, kā šos VBS izpildīt. Taču galvenā īpatnība bija tā, ka šie skripti, lietojot SimpleMAPI un CDO, mēģināja Outlook Address Book meklēt adreses, uz kurām pārsūtīties tālāk, lai izplatītu vīrusu. Microsoft-ieši izdomāja šo problēmu “nocirst pašā saknē”, proti, izveidot drošības ielāpu, kas aizliedz šiem skriptiem piekļūt noteiktiem MAPI laukiem. Ne gluži aizliegt, bet, tikko programma mēģināja nolasīt kāda “aizliegtā” lauka vērtību, tā uz ekrāna parādījās jautājums “Šī programma grib darīt sliktas lietas. Vai atļaut?”… un podziņas ar iespējām “yes/no”. Viss jau būtu jauki, tikai – kur parādīsies paziņojums, ja kods tiek izpildīts kā MSWindows serviss? Nu, tieši tā, nekur neparādīsies! Un, pat ja parādītos uz servera ekrāna, kurš tad to visu laiku spaidītu?

Pilna informācija par to, kuri MAPI lauki tiek bloķēti ar zināmo security patch un kāda funkcionalitāte vairs nav pieejama, atrodama Microsoft Knowledge Base. MapiLab arī publicējuši apakstu, kā izkļūt no situācijas, lai neredzētu drošības paziņojumus. Cik saprotu, viens no pagaidām labākajiem veidiem ir lietot Outlook Redemption, kas patiesībā ir virsbūve virs MAPI.dll bibliotēkas, uz kuru šis patch neattiecas. Bet tā ir trešās puses slēgta koda bibliotēka, kas bez maksas pieejama tikai izstrādes mērķiem.

Ir arī citas iespējas, proti, šos aizliegumus var “atspējot” kādam noteiktam lietotāja profilam. To var izdarīt MS Exchange administrators norādot, ka dotajam profilam MAPI security patch nav spēkā. Papildus tam uz lietotāja datora jāizdara izmaiņas Windows reģistrā:

[HKEYCURRENTUSER\Software\Policies\Microsoft\Security]
"CheckAdminSettings"=dword:00000001

Kad šīs izmaiņas ir veiktas, jāpārstartē MS Outlook un varam darboties tālāk. Minētais koda fragments, kas izdrukā arī vēstules sūtītāju, izpildās veiksmīgi.

Piekļuve MSExchange profiliem

Bet ja nu mēs iedomājamies palasīt vēstules kādā citā publiskajā folderī, kurā var iesūtīt gan vēstules no ārpuses, gan arī no paša MS Exchange? Bammm… kļūda. Kā izrādās darbs ar Sender atribūtu nebūt nav triviāls gadījumā, kad par to ir zināms kas vairāk nekā, piemēram “Kriss Rauhvargers <[email protected]>”, proti, ja lietotājs ir MS Exchange lietotājs. Šajā gadījumā lietotāja informācija ir sinhrona ar MS Active Directory ierakstu par lietotāju un, kā zināms, AD vienam un tam pašam lietotājam var reģistrēt daudzas e-pasta adreses. Bez visa šeit minētā vismaz pie tādas konfigurācijas, kāda pieejama man, Sender atribūta lielākā daļa atribūtu man nebija pieejami (un apskatāmi atkļūdošanas laikā tos apskatot, meta kļūdas).

Microsoft iesaka interesantu šīs problēmas apkārtceļu. Proti, ņemam vēstules objektu, veidojam tam Reply (itin kā taisītos sūtīt atbildi) un pēc tam no iegūtās atbildes vēstules nokopējam saņēmēju. Atbildi izmetam. Un, lūk, šim te objektam jau ir pieejams Name atribūts un viss pārējais nepieciešamais. Kodā tas izskatās šādi:

Private Function MessageSender(ByVal opMsg As CDO.Message) As String
    Dim sAddr As String = ""
    Dim oMsgRepl As CDO.Message
    Dim oRec As CDO.Recipient
    Dim bRightsOk As Boolean = False
    Try
        'mēģina izveidot reply vēstuli
        oMsgRepl = CType(opMsg.Reply(), CDO.Message)
        oRec = CType(CType(oMsgRepl.Recipients, CDO.Recipients).Item(1), CDO.Recipient)
        sAddr = UCase(CStr(oRec.Address()))   'paņem atbildes saņēmēja adresi
        bRightsOk = True
    Catch ex As Exception
        sAddr = Nothing 'dabūta kļūda skatoties uz e-pasta adresi
    Finally
        'outlook security brīdinājums mēģināts izmest servisam, 
        'bet tas nav iespējams. Izpilde uzreiz nokļuvusi šeit.
        If Not bRightsOk Then sAddr = Nothing
    End Try
    Return sAddr
End Function

Funkcijas rezultāts atgriež saņēmēja adresi, kas gan ir nedaudz neparasta. Gadījumā, ja saņēmējs ir no ārpuses, adrese būs, piemēram SMTP:[email protected], savukārt tad, kad lietotājs ir MS Exchange lietotājs, tā būs, piemēram, EX:/O=Org/OU=SomeUnit/CN=RECIPIENTS/CN=Katalogs/CN=Kriss (ieraksts atbilst Active Directory shēmai. Tomēr skaidrs, ka no šī “izlobīt” vajadzīgos fragmentus ir pavisam vienkārši. Šī funkcija bez kļūdām (bet arī bez pozitīviem rezultātiem) apstrādā arī situāciju, kad nav ir ieslēgta Outlook Security un Recipient objekts nav pieejams. Tādā brīdī ir novērojama interesanta anomālija, kad koda izpilde pēc rindas, kur paņem atbildes saņēmēja adresi, izpildes uzreiz nokļūst Finally blokā, bet ne Catch blokā, ja kods tiek izpildīts kā windows serviss. Zinu, tas izklausās neiespējami, jo patiesībā bija vai nu a) jānotiek kļūdai b) jāizpildās nākamjai rindai, tomēr tā tas nav.

Tādas kā beigas

Ar šo laikam varu arī beigt savu aprakstu. Jā, galvenais- neaizmirst pasaukt Logoff sesijas objektam. Tikai uzmanīgi – arī tajā brīdī var kaut kas “uzkārties” :-)

Downloads

Rakstā minēto koda paraugu pilns apkopojums vienā strādājošā funkcijā.

2 thoughts on “MsExchange piekļuve caur MAPI”

  1. Cik patestēju, man izdevās tikt pie kalendāra šādi:

    Dim objSession As CDO.Session Dim objCalendarFolder As CDO.Folder Dim objAppointments As CDO.Messages Dim objAppointment As CDO.AppointmentItem Try objSession = New CDO.Session objSession.Logon(NewSession:=True, NoMail:=False, ProfileInfo:=”mailserver”& vbLf & “user”) objCalendarFolder = objSession.GetDefaultFolder _ (CDO.CdoDefaultFolderTypes.CdoDefaultFolderCalendar) objAppointments = objCalendarFolder.Messages objAppointment = objAppointments.Item(1) Catch ex As Exception MsgBox(ex.ToString) Finally objSession.Logoff() End Try

Comments are closed.