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 visuspublic folders
atribūtus oFields = CType(oInfoStore.Fields, MAPI.Fields) 'atrodam atribūtosPublic 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.MessageFilteroMessages = 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 <kriss@naivist.net>”, 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:kriss@naivist.net, 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” :-)
Varbūt zini vai ir iespējams piekļūt Outlook kalendāram izmantojot CDO ?
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